Compare commits
348 Commits
2
...
2a99e2ea10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a99e2ea10 | ||
|
|
390202b9e4 | ||
|
|
65dae4f3ab | ||
|
|
df49631401 | ||
|
|
90e187b65c | ||
|
|
a92352f2b0 | ||
|
|
214b52badd | ||
| 581295443a | |||
|
|
914e8c70f2 | ||
| 7443fef6de | |||
| d734ce1dcc | |||
| 6c0a461a2b | |||
|
|
f2295717ee | ||
|
|
fd7bcde9d9 | ||
| fb8d905fb0 | |||
| f702c13422 | |||
| bf1088d0c5 | |||
| 51edcf9fce | |||
| 0826b0749d | |||
| aa3aa932fe | |||
| 5755730093 | |||
| de3ff211a6 | |||
| e2b95edeb8 | |||
| 92abc0d9e7 | |||
| d7d38d9b08 | |||
| 9c11698660 | |||
|
|
d64042fe46 | ||
|
|
5754f6a509 | ||
|
|
d5fbc08364 | ||
|
|
b2a2da0f97 | ||
|
|
c2db2ad583 | ||
|
|
2248a61936 | ||
|
|
39dfe3d52e | ||
|
|
1ffd92758d | ||
|
|
ad33ee6c86 | ||
|
|
d025287785 | ||
|
|
e0c48073f1 | ||
|
|
7fe49775b9 | ||
|
|
2225722ab5 | ||
|
|
aeaa54583a | ||
|
|
f23c8a3360 | ||
|
|
a47d4ee0a6 | ||
|
|
95d7e8d13d | ||
|
|
f87a40fbd6 | ||
|
|
627eeeb29b | ||
|
|
c464c31140 | ||
|
|
7c3f04e7f3 | ||
|
|
1c6e908f28 | ||
|
|
cec7d69199 | ||
|
|
38fc52241f | ||
|
|
15cef26d81 | ||
|
|
aaa745baf4 | ||
|
|
12f9100b17 | ||
|
|
bcfa30d5de | ||
|
|
7edb8f22cb | ||
|
|
4eb74ae968 | ||
|
|
7819f1a897 | ||
|
|
3ec821ae6a | ||
|
|
b54904a9f2 | ||
|
|
abfe497ddc | ||
|
|
f46e36b539 | ||
|
|
eedd5fbd46 | ||
|
|
86e7a60642 | ||
|
|
dfd4bed221 | ||
|
|
3a2b8a2bba | ||
|
|
0062d495c9 | ||
|
|
7dc78acb73 | ||
|
|
1e3d65e0e4 | ||
|
|
bfd5a63cb4 | ||
|
|
6e2e1944b6 | ||
|
|
2edca09859 | ||
|
|
7bb891187b | ||
|
|
a14cba63e6 | ||
|
|
ebcedbdea1 | ||
|
|
45a4dde034 | ||
|
|
553d2d5707 | ||
|
|
1e54d8b871 | ||
|
|
df6c6912d9 | ||
|
|
64ddb7d0b8 | ||
|
|
7c763ad5b4 | ||
|
|
1c6607fa6e | ||
|
|
2bbe166d63 | ||
|
|
2573db1bbf | ||
|
|
1dfb46c38c | ||
| f1c8633345 | |||
|
|
76ea5f8255 | ||
|
|
59a6deef73 | ||
|
|
ddfd3d3ac7 | ||
|
|
2a350a2ec8 | ||
|
|
37bc8f960a | ||
|
|
5754a69e0b | ||
|
|
69c7ffd962 | ||
|
|
9dbca25b4d | ||
|
|
aaf0c95925 | ||
|
|
3b91fb09ea | ||
| c5ae30507b | |||
|
|
26dcbc1dc9 | ||
| 9d98ac9e83 | |||
| 7608faf396 | |||
| 63cb8e9ee9 | |||
|
|
0afeff8227 | ||
|
|
432387982e | ||
|
|
c7adec20db | ||
|
|
725a1386c3 | ||
|
|
0fc9c85503 | ||
|
|
593ed1ab09 | ||
|
|
2fb1411b78 | ||
|
|
d8ac8521d4 | ||
|
|
2dcd5cc9a6 | ||
|
|
0c40b326ea | ||
|
|
5d20d1a25e | ||
|
|
c21168ad2d | ||
|
|
9fe03727c5 | ||
|
|
9f4d57a677 | ||
|
|
9b636f1711 | ||
|
|
7f7859c2a4 | ||
|
|
3394fef79b | ||
|
|
2504997c53 | ||
|
|
5ef4ff7c91 | ||
|
|
cb03898986 | ||
|
|
e533424de5 | ||
|
|
b2fcff303b | ||
|
|
c7270bcff7 | ||
|
|
3452b0fb69 | ||
|
|
94fd40618e | ||
|
|
634796b012 | ||
|
|
31247b7915 | ||
|
|
0de3c164fb | ||
|
|
645b075f89 | ||
|
|
fb495197b8 | ||
|
|
dc71c4f1cb | ||
|
|
dd9e4afa49 | ||
|
|
42cb117a8a | ||
|
|
c336a893a0 | ||
|
|
188792328e | ||
|
|
218001399c | ||
|
|
de91089743 | ||
|
|
a45bc150e8 | ||
|
|
d7c09e230f | ||
|
|
3d2fb61df5 | ||
|
|
b152f95ef8 | ||
|
|
22f1781ea2 | ||
|
|
872dbedb6e | ||
|
|
5c7bb82a7b | ||
|
|
48bb43d9a8 | ||
|
|
7c9db3f360 | ||
|
|
b050a6895b | ||
|
|
2003ee01a5 | ||
|
|
5be696db9d | ||
|
|
3a1aaa9b61 | ||
|
|
54f8e23488 | ||
|
|
65477592af | ||
|
|
a6d32f848e | ||
|
|
ee7867b36f | ||
|
|
374dd26855 | ||
|
|
d19b6250ae | ||
|
|
e2e568bcda | ||
|
|
318e3e3216 | ||
|
|
93bf2193d2 | ||
|
|
fdecf2fe3c | ||
|
|
2445b39f3b | ||
|
|
8f21491c72 | ||
|
|
c8b67a2fc8 | ||
|
|
3083096cd7 | ||
|
|
cf0882575e | ||
|
|
fc63836cc7 | ||
|
|
992f4f4f18 | ||
|
|
aaf14264d8 | ||
|
|
0b202722e6 | ||
|
|
1aa8c2dc87 | ||
|
|
85531da9eb | ||
|
|
34e79aa753 | ||
|
|
65c5e6572e | ||
|
|
ff5b13e717 | ||
|
|
2b9ea6ccec | ||
|
|
41fa98e8b7 | ||
|
|
8884159edc | ||
|
|
1e5de2061a | ||
|
|
095a8e89e4 | ||
|
|
24949b1cb2 | ||
|
|
141f17859a | ||
|
|
01d73c5fae | ||
|
|
78c5442381 | ||
|
|
550061435e | ||
|
|
5ebcb56b65 | ||
|
|
af2578573c | ||
|
|
f6b99f96a4 | ||
|
|
d25c2dd862 | ||
|
|
a7e51aac3f | ||
|
|
f38f1216ea | ||
|
|
eb1fc6d2ae | ||
|
|
2f0838e552 | ||
|
|
deb1688d0c | ||
|
|
93e68fe20f | ||
|
|
815f344d19 | ||
|
|
a69c008f96 | ||
|
|
8ee7814a45 | ||
|
|
2bd45f8654 | ||
|
|
59f5a695d8 | ||
|
|
9575edcfc4 | ||
|
|
d1dc65e13b | ||
|
|
ff1e1a73dd | ||
|
|
7a2723fef7 | ||
|
|
0c4b28ea45 | ||
|
|
cca53bbc96 | ||
|
|
4714e2259b | ||
|
|
d56c85bca9 | ||
|
|
49482830f9 | ||
|
|
77e1d3f293 | ||
|
|
65613af195 | ||
|
|
8154b2639d | ||
|
|
7f2f0caaa1 | ||
|
|
8e5db24bad | ||
|
|
79f2d4f020 | ||
|
|
8c271f639a | ||
|
|
b1fbc3cd8f | ||
|
|
84665e0c5d | ||
|
|
3b7c539a56 | ||
|
|
600188a99a | ||
|
|
a6068f7e2b | ||
|
|
0827da594c | ||
|
|
a693e35177 | ||
|
|
08fc8f50f3 | ||
|
|
8e5662c56f | ||
|
|
45123d5b6f | ||
|
|
9cd7b6920a | ||
|
|
83519ac67b | ||
|
|
04c54d0733 | ||
|
|
0ac56c5b92 | ||
|
|
48bf50c340 | ||
|
|
467bb71736 | ||
|
|
bfc38c2d43 | ||
|
|
5ae01a9ff2 | ||
|
|
b33bd3eb18 | ||
|
|
c42f979082 | ||
|
|
32486a6e77 | ||
|
|
5c4663c8ec | ||
|
|
a8b6e46f61 | ||
|
|
574fe5e860 | ||
|
|
dcc4936428 | ||
|
|
ce9b09f096 | ||
|
|
98daa93abd | ||
|
|
8dae04c32c | ||
|
|
3f4712bb44 | ||
|
|
6e37477e4f | ||
|
|
bdebbb9348 | ||
|
|
45af7e1649 | ||
|
|
a07bd3d388 | ||
|
|
1ef7c62ab4 | ||
|
|
a1d5a2c3a3 | ||
|
|
24cd2ccea3 | ||
|
|
d19bcb844a | ||
|
|
86b4641558 | ||
|
|
da4bb47ee5 | ||
|
|
4dd3c1bbf0 | ||
|
|
2ce2aad956 | ||
|
|
4f6c06a488 | ||
|
|
7890d0992a | ||
|
|
a30bd69bf2 | ||
|
|
42af45f92c | ||
|
|
d68a1da2c2 | ||
|
|
b2520726dc | ||
|
|
c70b2ee7be | ||
|
|
993c0c54b0 | ||
|
|
4b704d48fa | ||
|
|
470546ae82 | ||
|
|
1d4d054c75 | ||
|
|
92704a86ca | ||
|
|
1ac05494d6 | ||
|
|
6fa0e1638d | ||
|
|
89632228cc | ||
|
|
7dc3064a88 | ||
|
|
0cf5341d73 | ||
|
|
e233ee4bea | ||
|
|
3fecb9390f | ||
|
|
4ded4ea50a | ||
|
|
157ac05632 | ||
|
|
1ce023552c | ||
|
|
e656760441 | ||
|
|
f851dbdefd | ||
|
|
a055131ec7 | ||
| f890369684 | |||
| a7f43a4e74 | |||
| a06202d297 | |||
| 0b3beca147 | |||
| f212e7203e | |||
| 212840d4ab | |||
| 52bd2dfd85 | |||
| c1118fc335 | |||
| 54ee08b3c5 | |||
| ef922b2b90 | |||
| 479fd8bbd8 | |||
| 7f5e76551c | |||
| 33eda43664 | |||
| e64697b64a | |||
| 90e0bde7f1 | |||
| 1a8348cf4b | |||
| ee42d80d93 | |||
| fcd2a5e8a9 | |||
| 8aa36e3764 | |||
| 0ff82d1bac | |||
| 15381c3051 | |||
| e717fc1763 | |||
| df927e141d | |||
| a699037826 | |||
| c3819ccd65 | |||
| 0cd20850e0 | |||
| 4963b06e21 | |||
| ce0fb79b39 | |||
| 46e5a5eeee | |||
| 67872f823c | |||
| c9bc811bc1 | |||
| 7df8a1a1d2 | |||
| 6f6af7553a | |||
| 854767a2c2 | |||
| d71a81f279 | |||
| 5b9436b994 | |||
| d3903c913b | |||
| 38fcfd730c | |||
| e672ac8d3f | |||
| b7f987f2c8 | |||
| f169b6569a | |||
| d93d3b3c6d | |||
| 05498904cf | |||
| 5c8b82df15 | |||
| d769dec2ec | |||
| 5ac9c785f9 | |||
| bb73a492cc | |||
| 5ebded322f | |||
| c093fa46c1 | |||
| c368b6cb73 | |||
| 53049d1588 | |||
|
|
e67bc17fa2 | ||
|
|
16845d0ce2 | ||
|
|
177d082d80 | ||
|
|
d96381fd4e | ||
| 854c832c07 | |||
| 54cc658f0b | |||
| f80ce51859 | |||
| 7ce545fedb | |||
| f9da920e6b | |||
| ca3a109c41 | |||
| 4aca437d2b | |||
| d00f0d08e5 | |||
| 72770ad73b | |||
| 61ddf81fb0 | |||
| 004012b59f | |||
| 461643079b |
@@ -1,2 +0,0 @@
|
||||
# 3. 在项目的 `__init__.py` 文件(与manage.py同级)中,添加两行代码
|
||||
# 文件路径:a_long_dianjing/__init__.py
|
||||
@@ -14,6 +14,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
SECRET_KEY = app_secrets.SECRET_KEY
|
||||
DEBUG = False
|
||||
# 详细错误日志开关:True=在 except 块中输出完整堆栈到日志;False=仅记录错误摘要
|
||||
DEBUG_DETAIL_LOG = True
|
||||
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'www.abas.asia']
|
||||
|
||||
# ==================== 应用配置 ====================
|
||||
@@ -38,10 +40,15 @@ INSTALLED_APPS = [
|
||||
'utils',
|
||||
'shop',
|
||||
'rank',
|
||||
'merchant_ops',
|
||||
'jituan',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'utils.ip_filter_middleware.IPFilterMiddleware',
|
||||
'utils.debug_middleware.DebugExceptionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'jituan.middleware.ClubContextMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
@@ -51,6 +58,45 @@ MIDDLEWARE = [
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
# 日志配置 — 500 错误堆栈输出到 console,受 DEBUG_DETAIL_LOG 开关控制
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '[%(asctime)s] %(levelname)s %(name)s: %(message)s'
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
# 业务模块日志 — DEBUG_DETAIL_LOG=True 时输出 DEBUG 级别(含完整堆栈)
|
||||
'users': {
|
||||
'handlers': ['console'],
|
||||
'level': 'DEBUG' if DEBUG_DETAIL_LOG else 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'orders': {
|
||||
'handlers': ['console'],
|
||||
'level': 'DEBUG' if DEBUG_DETAIL_LOG else 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'utils.debug_middleware': {
|
||||
'handlers': ['console'],
|
||||
'level': 'DEBUG',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ROOT_URLCONF = 'a_long_dianjing.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
@@ -119,8 +165,8 @@ SIMPLE_JWT = {
|
||||
'LEEWAY': 0,
|
||||
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
|
||||
'USER_ID_FIELD': 'yonghuid',
|
||||
'USER_ID_CLAIM': 'yonghuid',
|
||||
'USER_ID_FIELD': 'UserUID',
|
||||
'USER_ID_CLAIM': 'UserUID',
|
||||
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
|
||||
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
|
||||
'TOKEN_TYPE_CLAIM': 'token_type',
|
||||
@@ -132,12 +178,13 @@ AUTHENTICATION_BACKENDS = [
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = 'users.UserMain'
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
|
||||
# ==================== DRF 配置 ====================
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'gvsdsdk.auth.jwt_auth.CompatibleJWTAuthentication',
|
||||
'gvsdsdk.auth.elt_auth.ELTAuthentication',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
@@ -177,8 +224,28 @@ CORS_ALLOW_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
||||
CORS_ALLOW_HEADERS = [
|
||||
"accept", "accept-encoding", "authorization", "content-type",
|
||||
"dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with",
|
||||
"x-club-id", "x-club-scope",
|
||||
]
|
||||
|
||||
# ==================== IP 访问控制 ====================
|
||||
# 访问模式:
|
||||
# production — 生产模式(默认):放行所有 IP
|
||||
# whitelist — 白名单模式:仅允许白名单内 IP 访问,其余返回 403
|
||||
# blacklist — 黑名单模式:禁止黑名单内 IP 访问,其余放行
|
||||
IP_ACCESS_MODE = 'production'
|
||||
# 白名单(whitelist 模式下生效)— 支持单 IP 和 CIDR 网段
|
||||
IP_WHITELIST = [
|
||||
'111.32.64.171',
|
||||
'43.142.0.208',
|
||||
'223.104.7.77',
|
||||
'112.7.200.17'
|
||||
]
|
||||
# 黑名单(blacklist 模式下生效)— 支持单 IP 和 CIDR 网段
|
||||
IP_BLACKLIST = []
|
||||
|
||||
# 免过滤路径(任何模式下都放行,如管理后台、健康检查)
|
||||
IP_FILTER_EXEMPT_PATHS = []
|
||||
|
||||
# ==================== 腾讯云 COS ====================
|
||||
COS_SECRET_ID = app_secrets.COS_SECRET_ID
|
||||
COS_SECRET_KEY = app_secrets.COS_SECRET_KEY
|
||||
@@ -317,3 +384,9 @@ WECHAT_PAY_V3_CONFIG = {
|
||||
'NOTIFY_URL': 'https://www.abas.asia/hqhd/yonghu/tixiancallback',
|
||||
'TRANSFER_SCENE_ID': '1005',
|
||||
}
|
||||
|
||||
|
||||
# ==================== 支付宝支付 ====================
|
||||
ALIPAY_SANDBOX = app_secrets.ALIPAY_SANDBOX
|
||||
ALIPAY_PROD = app_secrets.ALIPAY_PROD
|
||||
ALIPAY_SANDBOX_CFG = app_secrets.ALIPAY_SANDBOX_CFG
|
||||
|
||||
@@ -24,6 +24,7 @@ urlpatterns = [
|
||||
path('houtai/', include('backend.urls')),
|
||||
path('shangdian/', include('shop.urls')),
|
||||
path('dengji/', include('rank.urls')),
|
||||
path('jituan/', include('jituan.urls')),
|
||||
]
|
||||
|
||||
# 重要说明:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 4.2.27 on 2026-04-06 23:31
|
||||
# Generated by Django 4.2.27 on 2026-06-18 03:51
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
@@ -8,289 +8,296 @@ class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AbnormalUserLog",
|
||||
name='AbnormalUserLog',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"account_id",
|
||||
models.CharField(
|
||||
db_index=True, max_length=20, verbose_name="账号ID(phone)"
|
||||
),
|
||||
),
|
||||
(
|
||||
"ip_address",
|
||||
models.GenericIPAddressField(
|
||||
blank=True, null=True, verbose_name="IP地址"
|
||||
),
|
||||
),
|
||||
(
|
||||
"real_ip",
|
||||
models.GenericIPAddressField(
|
||||
blank=True, null=True, verbose_name="真实IP(穿透代理后)"
|
||||
),
|
||||
),
|
||||
(
|
||||
"x_forwarded_for",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="",
|
||||
max_length=255,
|
||||
verbose_name="X-Forwarded-For",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_agent",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="",
|
||||
max_length=500,
|
||||
verbose_name="User-Agent",
|
||||
),
|
||||
),
|
||||
(
|
||||
"request_method",
|
||||
models.CharField(
|
||||
blank=True, default="", max_length=10, verbose_name="请求方法"
|
||||
),
|
||||
),
|
||||
(
|
||||
"request_path",
|
||||
models.CharField(
|
||||
blank=True, default="", max_length=500, verbose_name="请求路径"
|
||||
),
|
||||
),
|
||||
(
|
||||
"request_params",
|
||||
models.TextField(
|
||||
blank=True, default="", verbose_name="请求参数(GET/POST)"
|
||||
),
|
||||
),
|
||||
(
|
||||
"request_body",
|
||||
models.TextField(
|
||||
blank=True, default="", verbose_name="请求体(原始)"
|
||||
),
|
||||
),
|
||||
(
|
||||
"request_headers",
|
||||
models.TextField(
|
||||
blank=True, default="", verbose_name="请求头(JSON格式)"
|
||||
),
|
||||
),
|
||||
(
|
||||
"attempt_type",
|
||||
models.CharField(
|
||||
blank=True, default="", max_length=50, verbose_name="攻击类型"
|
||||
),
|
||||
),
|
||||
(
|
||||
"detail",
|
||||
models.TextField(blank=True, default="", verbose_name="详细描述"),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
|
||||
),
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('account_id', models.CharField(db_index=True, max_length=20, verbose_name='账号ID(phone)')),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP地址')),
|
||||
('real_ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='真实IP(穿透代理后)')),
|
||||
('x_forwarded_for', models.CharField(blank=True, default='', max_length=255, verbose_name='X-Forwarded-For')),
|
||||
('user_agent', models.CharField(blank=True, default='', max_length=500, verbose_name='User-Agent')),
|
||||
('request_method', models.CharField(blank=True, default='', max_length=10, verbose_name='请求方法')),
|
||||
('request_path', models.CharField(blank=True, default='', max_length=500, verbose_name='请求路径')),
|
||||
('request_params', models.TextField(blank=True, default='', verbose_name='请求参数(GET/POST)')),
|
||||
('request_body', models.TextField(blank=True, default='', verbose_name='请求体(原始)')),
|
||||
('request_headers', models.TextField(blank=True, default='', verbose_name='请求头(JSON格式)')),
|
||||
('attempt_type', models.CharField(blank=True, default='', max_length=50, verbose_name='攻击类型')),
|
||||
('detail', models.TextField(blank=True, default='', verbose_name='详细描述')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "异常用户日志",
|
||||
"verbose_name_plural": "异常用户日志",
|
||||
"db_table": "abnormal_user_log",
|
||||
'verbose_name': '异常用户日志',
|
||||
'verbose_name_plural': '异常用户日志',
|
||||
'db_table': 'abnormal_user_log',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Permission",
|
||||
name='LeaderDailyStats',
|
||||
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="更新时间"),
|
||||
),
|
||||
('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": "权限",
|
||||
"verbose_name_plural": "权限",
|
||||
"db_table": "permission",
|
||||
'verbose_name': '组长每日统计',
|
||||
'db_table': 'zuzhang_ri_tongji',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Role",
|
||||
name='ManagerDailyStats',
|
||||
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="更新时间"),
|
||||
),
|
||||
('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": "角色",
|
||||
"verbose_name_plural": "角色",
|
||||
"db_table": "role",
|
||||
'verbose_name': '管事每日统计',
|
||||
'db_table': 'guanshi_ri_tongji',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserRole",
|
||||
name='ManagerRenewalDailyStats',
|
||||
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="角色",
|
||||
),
|
||||
),
|
||||
('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": "用户角色关联",
|
||||
"verbose_name_plural": "用户角色关联",
|
||||
"db_table": "user_role",
|
||||
'verbose_name': '管事续费每日统计',
|
||||
'db_table': 'guanshi_xufei_ri_tongji',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RolePermission",
|
||||
name='MerchantDailyStats',
|
||||
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="角色",
|
||||
),
|
||||
),
|
||||
('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": "角色权限关联",
|
||||
"verbose_name_plural": "角色权限关联",
|
||||
"db_table": "role_permission",
|
||||
'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='权限描述')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', 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='角色描述')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', 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')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', 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)),
|
||||
('CreateTime', 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"),
|
||||
model_name='role',
|
||||
index=models.Index(fields=['role_code'], name='role_role_co_b9f497_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="permission",
|
||||
index=models.Index(
|
||||
fields=["perm_code"], name="permission_perm_co_56cbe3_idx"
|
||||
),
|
||||
model_name='playerdailystats',
|
||||
index=models.Index(fields=['Date', '-AcceptedOrderTotal'], name='dashou_ri_t_riqi_95a785_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="abnormaluserlog",
|
||||
index=models.Index(
|
||||
fields=["account_id"], name="abnormal_us_account_425899_idx"
|
||||
),
|
||||
model_name='playerdailystats',
|
||||
index=models.Index(fields=['Date', '-CompletedOrderTotal'], name='dashou_ri_t_riqi_ad9c1a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="abnormaluserlog",
|
||||
index=models.Index(
|
||||
fields=["ip_address"], name="abnormal_us_ip_addr_3d612b_idx"
|
||||
),
|
||||
model_name='playerdailystats',
|
||||
index=models.Index(fields=['Date', '-AcceptedAmount'], name='dashou_ri_t_riqi_5da703_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="abnormaluserlog",
|
||||
index=models.Index(
|
||||
fields=["create_time"], name="abnormal_us_create__ee16da_idx"
|
||||
),
|
||||
model_name='playerdailystats',
|
||||
index=models.Index(fields=['Date', '-CompletedAmount'], name='dashou_ri_t_riqi_0a3156_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="userrole",
|
||||
unique_together={("account_id", "role")},
|
||||
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="rolepermission",
|
||||
unique_together={("role", "permission")},
|
||||
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=['CreateTime'], name='abnormal_us_create__ee16da_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='userrole',
|
||||
unique_together={('account_id', 'role')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rolepermission',
|
||||
unique_together={('role', 'permission')},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.27 on 2026-06-19 19:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('backend', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameIndex(
|
||||
model_name='abnormaluserlog',
|
||||
new_name='abnormal_us_CreateT_38f96c_idx',
|
||||
old_name='abnormal_us_create__ee16da_idx',
|
||||
),
|
||||
]
|
||||
@@ -1,339 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-04-09 14:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("backend", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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="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")},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,54 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-04-10 18:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("backend", "0002_zuzhangritongji_shangjiaritongji_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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, "组长")],
|
||||
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")},
|
||||
},
|
||||
),
|
||||
]
|
||||
40
backend/migrations/0003_withdrawaldailystats_club_id.py
Normal file
40
backend/migrations/0003_withdrawaldailystats_club_id.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Max
|
||||
|
||||
|
||||
def _dedupe_withdrawal_daily(apps, schema_editor):
|
||||
Model = apps.get_model('backend', 'WithdrawalDailyStats')
|
||||
for row in Model.objects.values('club_id', 'Date', 'WithdrawalType').annotate(max_id=Max('id')):
|
||||
Model.objects.filter(
|
||||
club_id=row['club_id'],
|
||||
Date=row['Date'],
|
||||
WithdrawalType=row['WithdrawalType'],
|
||||
).exclude(id=row['max_id']).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('backend', '0002_rename_abnormal_us_create__ee16da_idx_abnormal_us_createt_38f96c_idx'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='withdrawaldailystats',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='withdrawaldailystats',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.RunPython(_dedupe_withdrawal_daily, migrations.RunPython.noop),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='withdrawaldailystats',
|
||||
unique_together={('club_id', 'Date', 'WithdrawalType')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='withdrawaldailystats',
|
||||
index=models.Index(fields=['club_id', 'Date', 'WithdrawalType'], name='tixian_ri_club_date_type_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-06-15 01:42
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('backend', '0003_tixianritongji'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rolepermission',
|
||||
name='permission',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.permission', verbose_name='权限'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rolepermission',
|
||||
name='role',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tixianritongji',
|
||||
name='leixing',
|
||||
field=models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], verbose_name='用户类型'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userrole',
|
||||
name='role',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色'),
|
||||
),
|
||||
]
|
||||
91
backend/migrations/0004_role_daily_club_id.py
Normal file
91
backend/migrations/0004_role_daily_club_id.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Max
|
||||
|
||||
|
||||
def _dedupe_role_daily(apps, schema_editor, model_name, user_field):
|
||||
Model = apps.get_model('backend', model_name)
|
||||
for row in Model.objects.values('club_id', user_field, 'Date').annotate(max_id=Max('id')):
|
||||
Model.objects.filter(
|
||||
club_id=row['club_id'],
|
||||
**{user_field: row[user_field], 'Date': row['Date']},
|
||||
).exclude(id=row['max_id']).delete()
|
||||
|
||||
|
||||
def _dedupe_merchant(apps, schema_editor):
|
||||
_dedupe_role_daily(apps, schema_editor, 'MerchantDailyStats', 'MerchantID')
|
||||
|
||||
|
||||
def _dedupe_player(apps, schema_editor):
|
||||
_dedupe_role_daily(apps, schema_editor, 'PlayerDailyStats', 'PlayerID')
|
||||
|
||||
|
||||
def _dedupe_leader(apps, schema_editor):
|
||||
_dedupe_role_daily(apps, schema_editor, 'LeaderDailyStats', 'LeaderID')
|
||||
|
||||
|
||||
def _dedupe_manager(apps, schema_editor):
|
||||
_dedupe_role_daily(apps, schema_editor, 'ManagerDailyStats', 'ManagerID')
|
||||
|
||||
|
||||
def _dedupe_manager_renewal(apps, schema_editor):
|
||||
_dedupe_role_daily(apps, schema_editor, 'ManagerRenewalDailyStats', 'ManagerID')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('backend', '0003_withdrawaldailystats_club_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='merchantdailystats',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='playerdailystats',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='leaderdailystats',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='managerdailystats',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='managerrenewaldailystats',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||
),
|
||||
migrations.RunPython(_dedupe_merchant, migrations.RunPython.noop),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='merchantdailystats',
|
||||
unique_together={('club_id', 'MerchantID', 'Date')},
|
||||
),
|
||||
migrations.RunPython(_dedupe_player, migrations.RunPython.noop),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='playerdailystats',
|
||||
unique_together={('club_id', 'PlayerID', 'Date')},
|
||||
),
|
||||
migrations.RunPython(_dedupe_leader, migrations.RunPython.noop),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='leaderdailystats',
|
||||
unique_together={('club_id', 'LeaderID', 'Date')},
|
||||
),
|
||||
migrations.RunPython(_dedupe_manager, migrations.RunPython.noop),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='managerdailystats',
|
||||
unique_together={('club_id', 'ManagerID', 'Date')},
|
||||
),
|
||||
migrations.RunPython(_dedupe_manager_renewal, migrations.RunPython.noop),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='managerrenewaldailystats',
|
||||
unique_together={('club_id', 'ManagerID', 'Date')},
|
||||
),
|
||||
]
|
||||
@@ -10,13 +10,14 @@ class Role(QModel):
|
||||
role_name = models.CharField(max_length=50, verbose_name='角色名称')
|
||||
description = models.CharField(max_length=200, blank=True, default='', verbose_name='角色描述')
|
||||
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'role'
|
||||
verbose_name = '角色'
|
||||
verbose_name_plural = '角色'
|
||||
managed = True
|
||||
indexes = [
|
||||
models.Index(fields=['role_code']),
|
||||
]
|
||||
@@ -32,13 +33,14 @@ class Permission(QModel):
|
||||
perm_code = models.CharField(max_length=100, unique=True, db_index=True, verbose_name='权限编码')
|
||||
perm_name = models.CharField(max_length=100, verbose_name='权限名称')
|
||||
description = models.CharField(max_length=200, blank=True, default='', verbose_name='权限描述')
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'permission'
|
||||
verbose_name = '权限'
|
||||
verbose_name_plural = '权限'
|
||||
managed = True
|
||||
indexes = [
|
||||
models.Index(fields=['perm_code']),
|
||||
]
|
||||
@@ -53,13 +55,14 @@ class RolePermission(QModel):
|
||||
id = models.AutoField(primary_key=True)
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色')
|
||||
permission = models.ForeignKey(Permission, on_delete=models.CASCADE, db_index=True, verbose_name='权限')
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'role_permission'
|
||||
verbose_name = '角色权限关联'
|
||||
verbose_name_plural = '角色权限关联'
|
||||
unique_together = [['role', 'permission']] # 防止重复授权
|
||||
managed = True
|
||||
unique_together = [['role', 'permission']]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.role.role_name} -> {self.permission.perm_name}"
|
||||
@@ -69,46 +72,38 @@ class RolePermission(QModel):
|
||||
class UserRole(QModel):
|
||||
"""用户角色关联表"""
|
||||
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='角色')
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'user_role'
|
||||
verbose_name = '用户角色关联'
|
||||
verbose_name_plural = '用户角色关联'
|
||||
unique_together = [['account_id', 'role']] # 同一用户同一角色只能有一条
|
||||
managed = True
|
||||
unique_together = [['account_id', 'role']]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.account_id} -> {self.role.role_name}"
|
||||
|
||||
|
||||
# ==================== 5. 异常用户日志 ====================
|
||||
class AbnormalUserLog(QModel):
|
||||
"""记录恶意尝试越权操作的用户信息"""
|
||||
# 账号信息
|
||||
account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号ID(phone)')
|
||||
|
||||
# 网络信息
|
||||
ip_address = 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')
|
||||
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_path = models.CharField(max_length=500, blank=True, default='', verbose_name='请求路径')
|
||||
request_params = models.TextField(blank=True, default='', verbose_name='请求参数(GET/POST)')
|
||||
request_body = models.TextField(blank=True, default='', verbose_name='请求体(原始)')
|
||||
request_headers = models.TextField(blank=True, default='', verbose_name='请求头(JSON格式)')
|
||||
|
||||
# 攻击特征
|
||||
attempt_type = models.CharField(max_length=50, blank=True, default='', verbose_name='攻击类型')
|
||||
detail = models.TextField(blank=True, default='', verbose_name='详细描述')
|
||||
|
||||
# 时间戳
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'abnormal_user_log'
|
||||
@@ -117,155 +112,222 @@ class AbnormalUserLog(QModel):
|
||||
indexes = [
|
||||
models.Index(fields=['account_id']),
|
||||
models.Index(fields=['ip_address']),
|
||||
models.Index(fields=['create_time']),
|
||||
models.Index(fields=['CreateTime']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.account_id} @ {self.ip_address} - {self.create_time}"
|
||||
return f"{self.account_id} @ {self.ip_address} - {self.CreateTime}"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ShangjiaRiTongji(QModel):
|
||||
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='商家用户ID')
|
||||
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
|
||||
# ==================== 6. 商家每日统计 ====================
|
||||
class MerchantDailyStats(QModel):
|
||||
"""商家每日统计表(原 ShangjiaRiTongji)"""
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
MerchantID = 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='日期')
|
||||
|
||||
# 派发统计
|
||||
paifa_dingdan_shu = models.IntegerField(default=0, verbose_name='派发订单总数')
|
||||
paifa_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='派发订单总金额')
|
||||
AssignedOrderCount = models.IntegerField(
|
||||
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='结算订单总数')
|
||||
jiesuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='结算总金额')
|
||||
# 结算统计
|
||||
SettledOrderCount = models.IntegerField(
|
||||
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='退款订单总数')
|
||||
tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额')
|
||||
# 退款统计
|
||||
RefundOrderCount = models.IntegerField(
|
||||
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:
|
||||
db_table = 'shangjia_ri_tongji'
|
||||
verbose_name = '商家每日统计'
|
||||
unique_together = [['yonghuid', 'riqi']] # 每人每天一条记录
|
||||
unique_together = [['club_id', 'MerchantID', 'Date']]
|
||||
indexes = [
|
||||
models.Index(fields=['riqi', '-paifa_jine']),
|
||||
models.Index(fields=['riqi', '-jiesuan_jine']),
|
||||
models.Index(fields=['riqi', '-paifa_dingdan_shu']),
|
||||
models.Index(fields=['Date', '-AssignedAmount']),
|
||||
models.Index(fields=['Date', '-SettledAmount']),
|
||||
models.Index(fields=['Date', '-AssignedOrderCount']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"商家{self.MerchantID} {self.Date}"
|
||||
|
||||
|
||||
|
||||
|
||||
class DashouRiTongji(QModel):
|
||||
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='打手用户ID')
|
||||
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
|
||||
# ==================== 7. 打手每日统计 ====================
|
||||
class PlayerDailyStats(QModel):
|
||||
"""打手每日统计表(原 DashouRiTongji)"""
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
PlayerID = 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='日期')
|
||||
|
||||
# 接单统计
|
||||
jiedan_zongliang = models.IntegerField(default=0, verbose_name='接单总量')
|
||||
chengjiao_zongliang = models.IntegerField(default=0, verbose_name='成交单总量')
|
||||
AcceptedOrderTotal = models.IntegerField(
|
||||
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='接单总金额')
|
||||
chengjiao_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='成交总金额')
|
||||
AcceptedAmount = models.DecimalField(
|
||||
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='退款订单量')
|
||||
tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额')
|
||||
RefundCount = models.IntegerField(
|
||||
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:
|
||||
db_table = 'dashou_ri_tongji'
|
||||
verbose_name = '打手每日统计'
|
||||
unique_together = [['yonghuid', 'riqi']]
|
||||
unique_together = [['club_id', 'PlayerID', 'Date']]
|
||||
indexes = [
|
||||
models.Index(fields=['riqi', '-jiedan_zongliang']),
|
||||
models.Index(fields=['riqi', '-chengjiao_zongliang']),
|
||||
models.Index(fields=['riqi', '-jiedan_zonge']),
|
||||
models.Index(fields=['riqi', '-chengjiao_zonge']),
|
||||
models.Index(fields=['Date', '-AcceptedOrderTotal']),
|
||||
models.Index(fields=['Date', '-CompletedOrderTotal']),
|
||||
models.Index(fields=['Date', '-AcceptedAmount']),
|
||||
models.Index(fields=['Date', '-CompletedAmount']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"打手{self.PlayerID} {self.Date}"
|
||||
|
||||
|
||||
class ZuzhangRiTongji(QModel):
|
||||
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='组长用户ID')
|
||||
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
|
||||
# ==================== 8. 组长每日统计 ====================
|
||||
class LeaderDailyStats(QModel):
|
||||
"""组长每日统计表(原 ZuzhangRiTongji)"""
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
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='邀请管事总数')
|
||||
shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额(分佣)')
|
||||
|
||||
# 可选明细
|
||||
fenyong_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分佣金额')
|
||||
InvitedManagerCount = models.IntegerField(
|
||||
default=0, db_column='yaoqing_guanshi_shu', verbose_name='邀请管事总数')
|
||||
TotalIncome = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, default=0.00,
|
||||
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:
|
||||
db_table = 'zuzhang_ri_tongji'
|
||||
verbose_name = '组长每日统计'
|
||||
unique_together = [['yonghuid', 'riqi']]
|
||||
unique_together = [['club_id', 'LeaderID', 'Date']]
|
||||
indexes = [
|
||||
models.Index(fields=['riqi', '-yaoqing_guanshi_shu']),
|
||||
models.Index(fields=['riqi', '-shouru_zonge']),
|
||||
models.Index(fields=['Date', '-InvitedManagerCount']),
|
||||
models.Index(fields=['Date', '-TotalIncome']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"组长{self.LeaderID} {self.Date}"
|
||||
|
||||
|
||||
# ==================== 9. 管事每日统计 ====================
|
||||
class ManagerDailyStats(QModel):
|
||||
"""管事每日统计表(原 GuanshiRiTongji)"""
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
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):
|
||||
yonghuid = models.CharField(max_length=7, db_index=True, 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(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额')
|
||||
InvitedPlayerCount = models.IntegerField(
|
||||
default=0, db_column='yaoqing_dashou_shu', verbose_name='邀请打手总数')
|
||||
RechargedPlayerCount = models.IntegerField(
|
||||
default=0, db_column='chongzhi_dashou_shu', verbose_name='充值打手数量')
|
||||
TotalIncome = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, default=0.00,
|
||||
db_column='shouru_zonge', verbose_name='收入总额')
|
||||
|
||||
class Meta:
|
||||
db_table = 'guanshi_ri_tongji'
|
||||
verbose_name = '管事每日统计'
|
||||
unique_together = [['yonghuid', 'riqi']]
|
||||
unique_together = [['club_id', 'ManagerID', 'Date']]
|
||||
indexes = [
|
||||
models.Index(fields=['riqi', '-yaoqing_dashou_shu']),
|
||||
models.Index(fields=['riqi', '-chongzhi_dashou_shu']),
|
||||
models.Index(fields=['riqi', '-shouru_zonge']),
|
||||
models.Index(fields=['Date', '-InvitedPlayerCount']),
|
||||
models.Index(fields=['Date', '-RechargedPlayerCount']),
|
||||
models.Index(fields=['Date', '-TotalIncome']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"管事{self.ManagerID} {self.Date}"
|
||||
|
||||
|
||||
# ==================== 10. 管事续费每日统计 ====================
|
||||
class ManagerRenewalDailyStats(QModel):
|
||||
"""管事续费每日统计表(原 GuanshiXufeiRiTongji)"""
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
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):
|
||||
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID')
|
||||
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
|
||||
|
||||
xufei_zongshu = models.IntegerField(default=0, verbose_name='今日续费总数') # 续费订单数(或续费打手数)
|
||||
xufei_shouyi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='续费收益金额')
|
||||
RenewalTotal = models.IntegerField(
|
||||
default=0, db_column='xufei_zongshu', verbose_name='今日续费总数')
|
||||
RenewalRevenue = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, default=0.00,
|
||||
db_column='xufei_shouyi', verbose_name='续费收益金额')
|
||||
|
||||
class Meta:
|
||||
db_table = 'guanshi_xufei_ri_tongji'
|
||||
verbose_name = '管事续费每日统计'
|
||||
unique_together = [['yonghuid', 'riqi']]
|
||||
unique_together = [['club_id', 'ManagerID', 'Date']]
|
||||
indexes = [
|
||||
models.Index(fields=['riqi', '-xufei_zongshu']),
|
||||
models.Index(fields=['riqi', '-xufei_shouyi']),
|
||||
models.Index(fields=['Date', '-RenewalTotal']),
|
||||
models.Index(fields=['Date', '-RenewalRevenue']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"管事续费{self.ManagerID} {self.Date}"
|
||||
|
||||
|
||||
# models.py 中添加
|
||||
|
||||
class TixianRiTongji(QModel):
|
||||
"""
|
||||
每日提现统计表(按身份统计当天提现总额)
|
||||
"""
|
||||
riqi = models.DateField(db_index=True, verbose_name='日期')
|
||||
leixing = models.IntegerField(
|
||||
# ==================== 11. 提现每日统计 ====================
|
||||
class WithdrawalDailyStats(QModel):
|
||||
"""每日提现统计表(原 TixianRiTongji),按俱乐部+身份统计当天提现到账总额"""
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
Date = models.DateField(
|
||||
db_index=True, db_column='riqi', verbose_name='日期')
|
||||
WithdrawalType = models.IntegerField(
|
||||
choices=[(1, '打手'), (2, '管事'), (3, '组长'),
|
||||
(4, '考核官'), (5, '打手押金'), (6, '商家余额')],
|
||||
verbose_name='用户类型'
|
||||
)
|
||||
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)')
|
||||
db_column='leixing', verbose_name='用户类型')
|
||||
total_amount = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)')
|
||||
|
||||
class Meta:
|
||||
db_table = 'tixian_ri_tongji'
|
||||
verbose_name = '每日提现统计'
|
||||
unique_together = [['riqi', 'leixing']] # 每天每个身份一条记录
|
||||
indexes = [models.Index(fields=['riqi', 'leixing'])]
|
||||
unique_together = [['club_id', 'Date', 'WithdrawalType']]
|
||||
indexes = [
|
||||
models.Index(fields=['club_id', 'Date', 'WithdrawalType']),
|
||||
models.Index(fields=['Date', 'WithdrawalType']),
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
@@ -32,7 +32,7 @@ class PopupPageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PopupPage
|
||||
fields = [
|
||||
'id', 'page_key', 'name', 'description',
|
||||
'id', 'club_id', 'page_key', 'name', 'description',
|
||||
'is_active', 'ignore_user_mute',
|
||||
'created_at', 'updated_at', 'popups'
|
||||
]
|
||||
259
backend/services/shop_copy_service.py
Normal file
259
backend/services/shop_copy_service.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
店铺商品目录复制服务:复制店铺商品类型、专区、商品(含 OSS 图片独立拷贝)。
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from products.models import Shangpin, ShangpinZhuanqu
|
||||
from shop.models import Dianpu, ShangpinLeixingDianpu
|
||||
from utils.oss_utils import copy_in_oss, delete_from_oss, _url_to_oss_key
|
||||
|
||||
logger = logging.getLogger('houtai')
|
||||
|
||||
COPY_MODE_REPLACE = 'replace'
|
||||
COPY_MODE_APPEND = 'append'
|
||||
|
||||
|
||||
class ShopCopyError(Exception):
|
||||
def __init__(self, message, code=400):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.code = code
|
||||
|
||||
|
||||
def resolve_dianpu_by_user_uid(user_uid):
|
||||
"""通过小程序用户 ID(YonghuPingzheng.yonghu,全局唯一)定位店铺"""
|
||||
uid = str(user_uid or '').strip()
|
||||
if not uid:
|
||||
raise ShopCopyError('用户ID不能为空')
|
||||
try:
|
||||
return Dianpu.objects.select_related('pingzheng').get(pingzheng__yonghu=uid)
|
||||
except Dianpu.DoesNotExist:
|
||||
raise ShopCopyError(f'未找到用户ID {uid} 对应的店铺', 404)
|
||||
|
||||
|
||||
def _gen_dest_key(target_dianpu_id, category, src_url):
|
||||
src_key = _url_to_oss_key(src_url)
|
||||
ext = 'png'
|
||||
if src_key and '.' in src_key:
|
||||
ext = src_key.rsplit('.', 1)[-1].lower() or 'png'
|
||||
ts = int(time.time() * 1000)
|
||||
rand = uuid.uuid4().hex[:8]
|
||||
return f"dianpu/shangpin/{category}/{target_dianpu_id}_{ts}_{rand}.{ext}"
|
||||
|
||||
|
||||
def _copy_image_url(src_url, target_dianpu_id, category):
|
||||
"""复制 OSS 图片到目标店铺独立路径,返回相对 URL"""
|
||||
if not src_url:
|
||||
return None
|
||||
dest_key = _gen_dest_key(target_dianpu_id, category, src_url)
|
||||
if not copy_in_oss(src_url, dest_key):
|
||||
raise ShopCopyError(f'图片复制失败: {src_url}')
|
||||
return dest_key
|
||||
|
||||
|
||||
def _clear_target_catalog_db_only(target_dianpu):
|
||||
"""全部覆盖模式:仅删除目标店铺商品目录 DB 记录,返回待清理的 OSS URL"""
|
||||
dianpu_id = target_dianpu.id
|
||||
oss_to_delete = []
|
||||
|
||||
products = list(Shangpin.query.filter(dianpu_id=dianpu_id))
|
||||
for p in products:
|
||||
if p.tupian_url:
|
||||
oss_to_delete.append(p.tupian_url)
|
||||
if p.guize_tupian:
|
||||
oss_to_delete.append(p.guize_tupian)
|
||||
if products:
|
||||
Shangpin.query.filter(dianpu_id=dianpu_id).delete()
|
||||
|
||||
ShangpinZhuanqu.query.filter(dianpu_id=dianpu_id).delete()
|
||||
|
||||
types = list(ShangpinLeixingDianpu.query.filter(dianpu_id=dianpu_id))
|
||||
for t in types:
|
||||
if t.tupian_url:
|
||||
oss_to_delete.append(t.tupian_url)
|
||||
if types:
|
||||
ShangpinLeixingDianpu.query.filter(dianpu_id=dianpu_id).delete()
|
||||
|
||||
return oss_to_delete
|
||||
|
||||
|
||||
def _delete_oss_urls(urls):
|
||||
for url in urls:
|
||||
if url:
|
||||
delete_from_oss(url)
|
||||
|
||||
|
||||
def _pre_copy_source_images(source_dianpu, target_dianpu):
|
||||
"""
|
||||
预复制源店铺所有图片到目标店铺 OSS 路径。
|
||||
返回结构化的预复制数据,供事务内写库使用。
|
||||
"""
|
||||
src_id = source_dianpu.id
|
||||
tgt_id = target_dianpu.id
|
||||
|
||||
source_types = list(
|
||||
ShangpinLeixingDianpu.query.filter(dianpu_id=src_id).order_by('paixu', 'id')
|
||||
)
|
||||
type_payloads = []
|
||||
for st in source_types:
|
||||
type_payloads.append({
|
||||
'source': st,
|
||||
'tupian_url': _copy_image_url(st.tupian_url, tgt_id, 'shangpinleixing'),
|
||||
})
|
||||
|
||||
source_zones = list(
|
||||
ShangpinZhuanqu.query.filter(dianpu_id=src_id).order_by('paixu', 'id')
|
||||
)
|
||||
zone_payloads = []
|
||||
for sz in source_zones:
|
||||
zone_payloads.append({'source': sz})
|
||||
|
||||
source_products = list(
|
||||
Shangpin.query.filter(dianpu_id=src_id).order_by('paixu', 'id')
|
||||
)
|
||||
product_payloads = []
|
||||
for sp in source_products:
|
||||
product_payloads.append({
|
||||
'source': sp,
|
||||
'tupian_url': _copy_image_url(sp.tupian_url, tgt_id, 'shangpin'),
|
||||
'guize_tupian': _copy_image_url(sp.guize_tupian, tgt_id, 'shangpinguize') if sp.guize_tupian else None,
|
||||
})
|
||||
|
||||
return type_payloads, zone_payloads, product_payloads
|
||||
|
||||
|
||||
def _insert_copied_catalog(target_dianpu, type_payloads, zone_payloads, product_payloads):
|
||||
"""在目标店铺插入复制后的类型、专区、商品,维护 ID 映射"""
|
||||
type_id_map = {}
|
||||
zone_id_map = {}
|
||||
|
||||
for item in type_payloads:
|
||||
st = item['source']
|
||||
new_type = ShangpinLeixingDianpu.query.create(
|
||||
dianpu_id=target_dianpu.id,
|
||||
gonggong_leixing_id=st.gonggong_leixing_id,
|
||||
jieshao=st.jieshao,
|
||||
tupian_url=item['tupian_url'],
|
||||
paixu=st.paixu,
|
||||
shangjia_zhuangtai=st.shangjia_zhuangtai,
|
||||
fengjin_zhuangtai=st.fengjin_zhuangtai,
|
||||
shenhe_zhuangtai=st.shenhe_zhuangtai,
|
||||
)
|
||||
type_id_map[st.id] = new_type.id
|
||||
|
||||
for item in zone_payloads:
|
||||
sz = item['source']
|
||||
new_dianpu_leixing_id = type_id_map.get(sz.dianpu_leixing_id)
|
||||
if not new_dianpu_leixing_id:
|
||||
raise ShopCopyError(f'专区「{sz.mingzi}」关联的商品类型在复制映射中缺失')
|
||||
new_zone = ShangpinZhuanqu.query.create(
|
||||
dianpu_id=target_dianpu.id,
|
||||
mingzi=sz.mingzi,
|
||||
dianpu_leixing_id=new_dianpu_leixing_id,
|
||||
leixing_id=sz.leixing_id,
|
||||
paixu=sz.paixu,
|
||||
shangjia_zhuangtai=sz.shangjia_zhuangtai,
|
||||
fengjin_zhuangtai=sz.fengjin_zhuangtai,
|
||||
shenhezhuangtai=sz.shenhezhuangtai,
|
||||
)
|
||||
zone_id_map[sz.id] = new_zone.id
|
||||
|
||||
copied_products = 0
|
||||
for item in product_payloads:
|
||||
sp = item['source']
|
||||
new_dianpu_leixing_id = type_id_map.get(sp.dianpu_leixing_id)
|
||||
new_zhuanqu_id = zone_id_map.get(sp.zhuanqu_id) if sp.zhuanqu_id else None
|
||||
if sp.dianpu_leixing_id and not new_dianpu_leixing_id:
|
||||
raise ShopCopyError(f'商品「{sp.biaoqian}」关联的商品类型在复制映射中缺失')
|
||||
if sp.zhuanqu_id and not new_zhuanqu_id:
|
||||
raise ShopCopyError(f'商品「{sp.biaoqian}」关联的专区在复制映射中缺失')
|
||||
|
||||
Shangpin.query.create(
|
||||
biaoqian=sp.biaoqian,
|
||||
jiage=sp.jiage,
|
||||
kucun=sp.kucun,
|
||||
leixing_id=sp.leixing_id,
|
||||
zhuanqu_id=new_zhuanqu_id,
|
||||
zhenshi_xiaoliang=0,
|
||||
duiwai_xiaoliang=0,
|
||||
jieshao=sp.jieshao,
|
||||
xiadan_xuzhi=sp.xiadan_xuzhi,
|
||||
guize_tupian=item['guize_tupian'],
|
||||
yaoqiuleixing=sp.yaoqiuleixing,
|
||||
huiyuan_id=sp.huiyuan_id,
|
||||
yongjin=sp.yongjin,
|
||||
kaioi_ewai_dashou_fencheng=sp.kaioi_ewai_dashou_fencheng,
|
||||
ewai_dashou_fencheng=sp.ewai_dashou_fencheng,
|
||||
tupian_url=item['tupian_url'],
|
||||
paixu=sp.paixu,
|
||||
shenhezhuangtai=sp.shenhezhuangtai,
|
||||
dianpu_id=target_dianpu.id,
|
||||
dianpu_leixing_id=new_dianpu_leixing_id,
|
||||
shangjia_zhuangtai=sp.shangjia_zhuangtai,
|
||||
fengjin_zhuangtai=sp.fengjin_zhuangtai,
|
||||
shenhe_zhuangtai=sp.shenhe_zhuangtai,
|
||||
)
|
||||
copied_products += 1
|
||||
|
||||
return {
|
||||
'types': len(type_payloads),
|
||||
'zones': len(zone_payloads),
|
||||
'products': copied_products,
|
||||
}
|
||||
|
||||
|
||||
def copy_shop_catalog(source_user_uid, target_user_uid, copy_mode=COPY_MODE_APPEND):
|
||||
"""
|
||||
复制店铺商品目录。
|
||||
:param source_user_uid: 源店铺小程序用户 ID(YonghuPingzheng.yonghu,唯一)
|
||||
:param target_user_uid: 目标店铺小程序用户 ID
|
||||
:param copy_mode: 'replace' 全部覆盖 | 'append' 补充追加
|
||||
"""
|
||||
if copy_mode not in (COPY_MODE_REPLACE, COPY_MODE_APPEND):
|
||||
raise ShopCopyError('copy_mode 无效,仅支持 replace 或 append')
|
||||
|
||||
source_dianpu = resolve_dianpu_by_user_uid(source_user_uid)
|
||||
target_dianpu = resolve_dianpu_by_user_uid(target_user_uid)
|
||||
|
||||
if source_dianpu.id == target_dianpu.id:
|
||||
raise ShopCopyError('源店铺与目标店铺不能相同')
|
||||
|
||||
# 1. 预复制 OSS 图片(DB 变更前完成,失败则目标店不受影响)
|
||||
type_payloads, zone_payloads, product_payloads = _pre_copy_source_images(
|
||||
source_dianpu, target_dianpu
|
||||
)
|
||||
|
||||
oss_cleanup = []
|
||||
# 2. 数据库事务:锁定目标店 → 可选清空 → 写入
|
||||
with transaction.atomic():
|
||||
Dianpu.objects.select_for_update().get(id=target_dianpu.id)
|
||||
|
||||
if copy_mode == COPY_MODE_REPLACE:
|
||||
oss_cleanup = _clear_target_catalog_db_only(target_dianpu)
|
||||
|
||||
stats = _insert_copied_catalog(target_dianpu, type_payloads, zone_payloads, product_payloads)
|
||||
|
||||
if oss_cleanup:
|
||||
_delete_oss_urls(oss_cleanup)
|
||||
|
||||
logger.info(
|
||||
'店铺商品复制完成: source=%s(%s) -> target=%s(%s) mode=%s stats=%s',
|
||||
source_dianpu.id, source_user_uid,
|
||||
target_dianpu.id, target_user_uid,
|
||||
copy_mode, stats,
|
||||
)
|
||||
|
||||
return {
|
||||
'source_dianpu_id': source_dianpu.id,
|
||||
'source_dianpu_name': source_dianpu.dianpu_mingcheng,
|
||||
'target_dianpu_id': target_dianpu.id,
|
||||
'target_dianpu_name': target_dianpu.dianpu_mingcheng,
|
||||
'copy_mode': copy_mode,
|
||||
'copied_types': stats['types'],
|
||||
'copied_zones': stats['zones'],
|
||||
'copied_products': stats['products'],
|
||||
}
|
||||
@@ -6,15 +6,17 @@ from .view import (GetRolePermissionView,
|
||||
ModifyAdminUserView, KefuGetDashouListView, KefuGetDashouDetailView, KefuUpdateDashouView, KefuGetShangjiaListView,
|
||||
KefuGetShangjiaDetailView, KefuUpdateShangjiaView, GetProductBaseDataView, GetProductListView, SaveProductOrderView,
|
||||
AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView, UpdateMemberView, GetMemberListView,
|
||||
AddMemberView, GetGuanliListView, GetGuanliDetailView, UpdateGuanliView, GetWithdrawSettingsView,
|
||||
UpdateWithdrawSettingsView, GetZuzhangListView, GetZuzhangDetailView, UpdateZuzhangView, GetProductTypeZoneView,
|
||||
AddMemberView, GetGuanliListView, GetGuanliDetailView, PromoteGuanshiToZuzhangView, ChangeInviterView, UpdateGuanliView, GetWithdrawSettingsView,
|
||||
UpdateWithdrawSettingsView, GetZuzhangListView, GetZuzhangDetailView, UpdateZuzhangView, GetOperationLogListView, GetProductTypeZoneView,
|
||||
ModifyProductTypeZoneView, GetRateView, ModifyRateView, PopupNoticeListAPIView, PopupNoticeModifyAPIView,ShopListView,
|
||||
ShopModifyView, ShopPublicTypeAndAuditView, ShopListForProductView, ShopProductModifyView,
|
||||
ShopProductTypeMappingView, ProductListView, ProductModifyView, KefuChatPermissionsView,
|
||||
ShopProductTypeMappingView, ProductListView, ProductModifyView, ShopCopyCatalogView, KefuChatPermissionsView,
|
||||
FineApplyView, PunishDashouView,
|
||||
FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView,
|
||||
FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView, FaKuanPingTaiShenHeView,
|
||||
HqbkxxView,BkxgView,CaiwuView,CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView,
|
||||
CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, ZxkfghdsView)
|
||||
CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, KhjlglView, KhjlczView, ZxkfghdsView)
|
||||
from config.views import MiniappScriptHoutaiListView, MiniappScriptHoutaiModifyView
|
||||
from rank.reward_houtai_views import RankRewardHoutaiListView, RankRewardHoutaiSaveView, RankRewardHoutaiClaimsView
|
||||
|
||||
urlpatterns = [
|
||||
path('gljshq', GetRolePermissionView.as_view(), name='获取就角色信息'),
|
||||
@@ -46,7 +48,10 @@ urlpatterns = [
|
||||
path('hthqgslb', GetGuanliListView.as_view(), name='后台获取管事列表'),
|
||||
|
||||
path('hthqgsxq', GetGuanliDetailView.as_view(), name='后台获取管事详情信息'),
|
||||
path('htgssjzz', PromoteGuanshiToZuzhangView.as_view(), name='后台管事升级组长'),
|
||||
path('htghyqr', ChangeInviterView.as_view(), name='后台更换邀请人'),
|
||||
path('htxggsxx', UpdateGuanliView.as_view(), name='后台更新管事数据'),
|
||||
path('hqczrz', GetOperationLogListView.as_view(), name='后台操作日志列表'),
|
||||
path('hthqtxpz', GetWithdrawSettingsView.as_view(), name='后台获取提现配置'),
|
||||
path('htxgtxsz', UpdateWithdrawSettingsView.as_view(), name='后台修改提现配置'),
|
||||
path('hthqzzlb', GetZuzhangListView.as_view(), name='后台获取组长列表'),
|
||||
@@ -60,6 +65,11 @@ urlpatterns = [
|
||||
path('htxgfl', ModifyRateView.as_view(), name='后台修改费率'),
|
||||
path('hthqtcxx', PopupNoticeListAPIView.as_view(), name='后台获取弹窗配置'),
|
||||
path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'),
|
||||
path('hthqhs', MiniappScriptHoutaiListView.as_view(), name='后台获取话术配置'),
|
||||
path('htxghs', MiniappScriptHoutaiModifyView.as_view(), name='后台更新话术配置'),
|
||||
path('phbjhq', RankRewardHoutaiListView.as_view(), name='后台排行榜奖励配置'),
|
||||
path('phbjbc', RankRewardHoutaiSaveView.as_view(), name='后台保存排行榜奖励'),
|
||||
path('phbjjl', RankRewardHoutaiClaimsView.as_view(), name='后台排行榜领取明细'),
|
||||
|
||||
path('hqdplbsj', ShopListView.as_view(), name='获取店铺列表数据'),
|
||||
path('xgdpxx', ShopModifyView.as_view(), name='修改或添加店铺数据'),
|
||||
@@ -72,6 +82,7 @@ urlpatterns = [
|
||||
|
||||
path('hqhtdpsplx', ProductListView.as_view(), name='后台获取店铺商品数据'),
|
||||
path('htxgdpspsj', ProductModifyView.as_view(), name='后台修改店铺商品数据'),
|
||||
path('htfzdpspsj', ShopCopyCatalogView.as_view(), name='后台复制店铺商品目录'),
|
||||
|
||||
path('kfhqltqx', KefuChatPermissionsView.as_view(), name='后台获取客服聊天配置'),
|
||||
|
||||
@@ -79,6 +90,7 @@ urlpatterns = [
|
||||
path('htglyhqcfsltj', FaKuanTongJiView.as_view(),name='后台罚单统计图片'), # 罚款统计
|
||||
path('hthqfklb', FaKuanLieBiaoView.as_view(),name='后台罚单列表'), # 罚款列表
|
||||
path('glyclfk', FaKuanChuLiView.as_view(),name='后台罚单操作'), # 处理罚款
|
||||
path('glyptshfk', FaKuanPingTaiShenHeView.as_view(), name='后台罚款平台审核'),
|
||||
path('htfksc', FaKuanChuangJianView.as_view(),name='后台生成罚单'), # 创建罚款
|
||||
path('kffkdssq', FineApplyView.as_view(), name='客服罚款申请'), # 客服罚款申请
|
||||
path('kpcf', PunishDashouView.as_view(), name='客服处罚打手'), # 客服处罚打手
|
||||
@@ -100,6 +112,8 @@ urlpatterns = [
|
||||
|
||||
path('khggl', KhgglView.as_view(), name='获取考核管信息'),
|
||||
path('shgxgsj', ShgxgsjView.as_view(), name='修改考核管数据'),
|
||||
path('khjlgl', KhjlglView.as_view(), name='考核记录列表与统计'),
|
||||
path('khjlcz', KhjlczView.as_view(), name='考核记录操作'),
|
||||
|
||||
path('zxkfghds', ZxkfghdsView.as_view(), name='链接换打手或重新下单'),
|
||||
|
||||
|
||||
558
backend/utils.py
558
backend/utils.py
@@ -5,8 +5,9 @@ from functools import lru_cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Q
|
||||
from rest_framework.response import Response
|
||||
from .models import AbnormalUserLog, UserRole, Permission, DashouRiTongji, ShangjiaRiTongji, \
|
||||
GuanshiXufeiRiTongji, GuanshiRiTongji, ZuzhangRiTongji, TixianRiTongji
|
||||
from .models import AbnormalUserLog, PlayerDailyStats, MerchantDailyStats, \
|
||||
ManagerRenewalDailyStats, ManagerDailyStats, LeaderDailyStats, WithdrawalDailyStats
|
||||
from gvsdsdk.models import UserRole as SdkUserRole, RolePermission as SdkRolePermission, Permission as SdkPermission
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
@@ -20,6 +21,16 @@ from gvsdsdk.fluent import db, func, FQ
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _club_id_for_yonghuid(yonghuid, club_id=None):
|
||||
from jituan.constants import CLUB_ID_DEFAULT
|
||||
from jituan.services.club_user import get_user_club_id
|
||||
from users.business_models import User
|
||||
if club_id:
|
||||
return club_id
|
||||
u = User.query.filter(UserUID=yonghuid).first()
|
||||
return get_user_club_id(u) if u else CLUB_ID_DEFAULT
|
||||
|
||||
|
||||
def _get_real_ip(request):
|
||||
"""获取真实IP(考虑代理)"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
@@ -60,9 +71,62 @@ def _record_abnormal(account_id, request, attempt_type, detail=''):
|
||||
logger.error(f"记录异常日志失败: {e}")
|
||||
|
||||
|
||||
class _AllPermissions(list):
|
||||
"""拥有 000001 超级管理权限时使用,使任何权限码 in 检查都返回 True"""
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
|
||||
def has_perm_code(permissions, *codes):
|
||||
"""权限码 in 检查;系统超管 _AllPermissions 视为全部通过。"""
|
||||
if isinstance(permissions, _AllPermissions):
|
||||
return True
|
||||
perms = permissions or []
|
||||
return any(c in perms for c in codes)
|
||||
|
||||
|
||||
def has_merchant_order_permission(permissions):
|
||||
"""商家/链接订单处理权限:002ac 商家订单、002ab 平台订单、003aa 跨平台。"""
|
||||
return has_perm_code(permissions, '002ac', '002ab', '003aa')
|
||||
|
||||
|
||||
def _legacy_backend_role_permissions(phone):
|
||||
"""
|
||||
读取旧权限表(backend.models):
|
||||
user_role.account_id = 手机号 → role → role_permission → permission.perm_code
|
||||
与 gvsdsdk 表并行,未改 SDK 结构。
|
||||
"""
|
||||
phone = (phone or '').strip()
|
||||
if not phone:
|
||||
return [], []
|
||||
try:
|
||||
from backend.models import (
|
||||
UserRole as LegacyUserRole,
|
||||
Role as BackendRole,
|
||||
Permission as BackendPermission,
|
||||
)
|
||||
role_ids = list(
|
||||
LegacyUserRole.objects.filter(account_id=phone).values_list('role_id', flat=True)
|
||||
)
|
||||
if not role_ids:
|
||||
return [], []
|
||||
perm_codes = list(
|
||||
BackendPermission.objects.filter(
|
||||
rolepermission__role_id__in=role_ids
|
||||
).values_list('perm_code', flat=True).distinct()
|
||||
)
|
||||
role_names = list(
|
||||
BackendRole.objects.filter(role_id__in=role_ids).values_list('role_name', flat=True)
|
||||
)
|
||||
return perm_codes, role_names
|
||||
except Exception as e:
|
||||
logger.warning('legacy_backend_role_permissions phone=%s err=%s', phone, e)
|
||||
return [], []
|
||||
|
||||
|
||||
def verify_kefu_permission(request, username_from_frontend=None):
|
||||
"""
|
||||
验证客服身份,并返回该用户的所有权限列表(基于新RBAC表)
|
||||
验证客服/管理员身份,并返回该用户的所有权限列表(基于新RBAC表)
|
||||
只有前端传递的username与当前登录用户不匹配时,才记录异常并警告前端。
|
||||
|
||||
参数:
|
||||
@@ -75,37 +139,93 @@ def verify_kefu_permission(request, username_from_frontend=None):
|
||||
"""
|
||||
current_user = request.user
|
||||
|
||||
# ---------- 1. 必须是客服 ----------
|
||||
if current_user.user_type != 'kefu':
|
||||
# ---------- 1. 必须是客服或管理员 ----------
|
||||
from jituan.constants import SUPER_ADMIN_PHONES
|
||||
from jituan.services.admin_context import is_kefu_backend_account
|
||||
is_super_phone = getattr(current_user, 'Phone', '') in SUPER_ADMIN_PHONES
|
||||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser or is_super_phone
|
||||
if not is_kefu_backend_account(current_user):
|
||||
return None, Response({'code': 403, 'msg': '身份错误,非客服账号'}, status=403)
|
||||
|
||||
# ---------- 2. 客服扩展表存在且状态正常 ----------
|
||||
# ---------- 2. 客服扩展表存在且状态正常(管理员跳过) ----------
|
||||
kefu = None
|
||||
if not is_admin:
|
||||
try:
|
||||
kefu = current_user.kefu_profile
|
||||
kefu = current_user.KefuProfile
|
||||
if kefu.zhuangtai != 1:
|
||||
return None, Response({'code': 403, 'msg': '客服账号已被禁用'}, status=403)
|
||||
except ObjectDoesNotExist:
|
||||
return None, Response({'code': 403, 'msg': '客服账号不存在'}, status=403)
|
||||
else:
|
||||
# 管理员无需客服扩展表,用占位对象以通过 kefu_obj is None 检查
|
||||
from types import SimpleNamespace
|
||||
kefu = SimpleNamespace(user=current_user)
|
||||
|
||||
# ---------- 3. 越权检测(只记录前端篡改username的情况) ----------
|
||||
if username_from_frontend and current_user.phone != username_from_frontend:
|
||||
detail = f'前端username: {username_from_frontend},实际登录账号: {current_user.phone}'
|
||||
_record_abnormal(current_user.phone, request, '越权尝试篡改username', detail=detail)
|
||||
if username_from_frontend and current_user.Phone != username_from_frontend:
|
||||
detail = f'前端username: {username_from_frontend},实际登录账号: {current_user.Phone}'
|
||||
_record_abnormal(current_user.Phone, request, '越权尝试篡改username', detail=detail)
|
||||
return None, Response({
|
||||
'code': 403,
|
||||
'msg': '身份错误,无权操作其他账号,相关操作已记录'
|
||||
}, status=403)
|
||||
|
||||
# ---------- 4. 获取用户的所有权限(通过角色) ----------
|
||||
# 步骤1:获取用户的所有角色ID
|
||||
role_ids = UserRole.query.filter(account_id=current_user.phone).values_list('role_id', flat=True)
|
||||
# 步骤2:通过角色ID获取权限编码
|
||||
permissions = list(Permission.query.filter(
|
||||
rolepermission__role_id__in=role_ids
|
||||
).values_list('perm_code', flat=True).distinct())
|
||||
# ---------- 4. 获取用户的所有权限(与菜单 menu-access 同一套算法) ----------
|
||||
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_SINGLE
|
||||
from jituan.services.admin_context import resolve_admin_effective_club, get_allowed_club_ids, is_system_super_admin
|
||||
from jituan.services.club_rbac import resolve_effective_permission_codes
|
||||
|
||||
eff_club = getattr(request, 'club_id', None) or CLUB_ID_DEFAULT
|
||||
eff_scope = getattr(request, 'club_scope', None) or DATA_SCOPE_SINGLE
|
||||
allowed_club_ids = None
|
||||
if not is_system_super_admin(current_user):
|
||||
eff_club, eff_scope, club_err = resolve_admin_effective_club(current_user, request)
|
||||
if club_err:
|
||||
return None, Response({'code': 403, 'msg': club_err}, status=403)
|
||||
request.club_id = eff_club
|
||||
request.club_scope = eff_scope
|
||||
allowed_club_ids = list(get_allowed_club_ids(current_user))
|
||||
|
||||
try:
|
||||
if is_system_super_admin(current_user):
|
||||
user_uuid = current_user.UserUUID
|
||||
role_uuids = list(SdkUserRole.objects.filter(
|
||||
UserUUID=user_uuid
|
||||
).values_list('RoleUUID', flat=True))
|
||||
if role_uuids:
|
||||
perm_uuids = list(SdkRolePermission.objects.filter(
|
||||
RoleUUID__in=role_uuids
|
||||
).values_list('PermUUID', flat=True))
|
||||
permissions = list(SdkPermission.objects.filter(
|
||||
PermUUID__in=perm_uuids, PermStatus=1
|
||||
).values_list('PermCode', flat=True).distinct())
|
||||
else:
|
||||
permissions = []
|
||||
legacy_codes, _ = _legacy_backend_role_permissions(current_user.Phone)
|
||||
if legacy_codes:
|
||||
permissions = list(set(list(permissions) + legacy_codes))
|
||||
else:
|
||||
permissions = resolve_effective_permission_codes(
|
||||
current_user, eff_club, eff_scope, allowed_club_ids,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
'verify_kefu_permission failed: UserUUID=%s Phone=%s err=%s',
|
||||
getattr(current_user, 'UserUUID', None),
|
||||
getattr(current_user, 'Phone', None),
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
permissions = []
|
||||
|
||||
# 仅系统级超管自动拥有全部功能权限;俱乐部 000001 只解锁超管页,不展开全部菜单
|
||||
if is_system_super_admin(current_user):
|
||||
if '000001' not in permissions:
|
||||
permissions.insert(0, '000001')
|
||||
permissions = _AllPermissions(permissions)
|
||||
|
||||
if not permissions:
|
||||
logger.warning(f"用户 {current_user.phone} 没有任何权限,请检查角色分配")
|
||||
logger.warning(f"用户 {current_user.Phone} 没有任何权限,请检查角色分配")
|
||||
|
||||
return kefu, permissions
|
||||
|
||||
@@ -113,139 +233,137 @@ def verify_kefu_permission(request, username_from_frontend=None):
|
||||
|
||||
|
||||
|
||||
def update_dashou_daily_by_action(yonghuid, amount, action):
|
||||
def update_dashou_daily_by_action(yonghuid, amount, action, club_id=None):
|
||||
"""
|
||||
根据行为类型更新打手每日统计(接单、成交、退款)
|
||||
|
||||
参数:
|
||||
yonghuid: 打手用户ID
|
||||
amount: 本单涉及的金额(Decimal)
|
||||
action: 1 = 接单, 2 = 成交(结算), 3 = 退款
|
||||
"""
|
||||
amount = Decimal(str(amount)) if amount else Decimal('0.00')
|
||||
if amount <= 0:
|
||||
return # 金额非正不统计
|
||||
return
|
||||
|
||||
cid = _club_id_for_yonghuid(yonghuid, club_id)
|
||||
today = date.today()
|
||||
|
||||
with transaction.atomic():
|
||||
stat, created = DashouRiTongji.objects.select_for_update().get_or_create(
|
||||
yonghuid=yonghuid,
|
||||
riqi=today,
|
||||
stat, created = PlayerDailyStats.objects.select_for_update().get_or_create(
|
||||
club_id=cid,
|
||||
PlayerID=yonghuid,
|
||||
Date=today,
|
||||
defaults={
|
||||
'yonghuid': yonghuid,
|
||||
'riqi': today,
|
||||
'jiedan_zongliang': 0,
|
||||
'jiedan_zonge': Decimal('0.00'),
|
||||
'chengjiao_zongliang': 0,
|
||||
'chengjiao_zonge': Decimal('0.00'),
|
||||
'tuikuan_liang': 0,
|
||||
'tuikuan_jine': Decimal('0.00'),
|
||||
'club_id': cid,
|
||||
'PlayerID': yonghuid,
|
||||
'Date': today,
|
||||
'AcceptedOrderTotal': 0,
|
||||
'AcceptedAmount': Decimal('0.00'),
|
||||
'CompletedOrderTotal': 0,
|
||||
'CompletedAmount': Decimal('0.00'),
|
||||
'RefundCount': 0,
|
||||
'RefundAmount': Decimal('0.00'),
|
||||
}
|
||||
)
|
||||
|
||||
update_fields = {}
|
||||
if action == 1: # 接单
|
||||
update_fields['jiedan_zongliang'] = F('jiedan_zongliang') + 1
|
||||
update_fields['jiedan_zonge'] = F('jiedan_zonge') + amount
|
||||
update_fields['AcceptedOrderTotal'] = F('AcceptedOrderTotal') + 1
|
||||
update_fields['AcceptedAmount'] = F('AcceptedAmount') + amount
|
||||
elif action == 2: # 成交
|
||||
update_fields['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1
|
||||
update_fields['chengjiao_zonge'] = F('chengjiao_zonge') + amount
|
||||
update_fields['CompletedOrderTotal'] = F('CompletedOrderTotal') + 1
|
||||
update_fields['CompletedAmount'] = F('CompletedAmount') + amount
|
||||
elif action == 3: # 退款
|
||||
update_fields['tuikuan_liang'] = F('tuikuan_liang') + 1
|
||||
update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount
|
||||
update_fields['RefundCount'] = F('RefundCount') + 1
|
||||
update_fields['RefundAmount'] = F('RefundAmount') + amount
|
||||
else:
|
||||
return # 无效行为
|
||||
|
||||
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, order_id=None, club_id=None):
|
||||
"""
|
||||
根据操作行为更新商家每日统计(派发、结算、退款)
|
||||
|
||||
参数:
|
||||
yonghuid: 商家用户ID
|
||||
amount: 当前订单涉及的金额(Decimal)
|
||||
action: 操作类型,1 = 派发,2 = 结算(成交),3 = 退款
|
||||
|
||||
说明:
|
||||
- 金额必须大于0,否则不执行任何操作
|
||||
- 使用 select_for_update + F() 表达式保证原子性和并发安全
|
||||
- 每天每个商家只会有一条统计记录,不存在则自动创建
|
||||
"""
|
||||
amount = Decimal(str(amount)) if amount else Decimal('0.00')
|
||||
if amount <= 0:
|
||||
return # 金额非正,不统计
|
||||
return
|
||||
|
||||
cid = _club_id_for_yonghuid(yonghuid, club_id)
|
||||
today = date.today()
|
||||
|
||||
with transaction.atomic():
|
||||
# 获取或创建当天统计记录,同时加锁防止并发
|
||||
stat, created = ShangjiaRiTongji.objects.select_for_update().get_or_create(
|
||||
yonghuid=yonghuid,
|
||||
riqi=today,
|
||||
stat, created = MerchantDailyStats.objects.select_for_update().get_or_create(
|
||||
club_id=cid,
|
||||
MerchantID=yonghuid,
|
||||
Date=today,
|
||||
defaults={
|
||||
'yonghuid': yonghuid,
|
||||
'riqi': today,
|
||||
'paifa_dingdan_shu': 0,
|
||||
'paifa_jine': Decimal('0.00'),
|
||||
'jiesuan_dingdan_shu': 0,
|
||||
'jiesuan_jine': Decimal('0.00'),
|
||||
'tuikuan_dingdan_shu': 0,
|
||||
'tuikuan_jine': Decimal('0.00'),
|
||||
'club_id': cid,
|
||||
'MerchantID': yonghuid,
|
||||
'Date': today,
|
||||
'AssignedOrderCount': 0,
|
||||
'AssignedAmount': Decimal('0.00'),
|
||||
'SettledOrderCount': 0,
|
||||
'SettledAmount': Decimal('0.00'),
|
||||
'RefundOrderCount': 0,
|
||||
'RefundAmount': Decimal('0.00'),
|
||||
}
|
||||
)
|
||||
|
||||
# 构建本次更新的字段,使用 F 表达式进行原子累加
|
||||
update_fields = {}
|
||||
if action == 1: # 派发
|
||||
update_fields['paifa_dingdan_shu'] = F('paifa_dingdan_shu') + 1
|
||||
update_fields['paifa_jine'] = F('paifa_jine') + amount
|
||||
update_fields['AssignedOrderCount'] = F('AssignedOrderCount') + 1
|
||||
update_fields['AssignedAmount'] = F('AssignedAmount') + amount
|
||||
elif action == 2: # 结算(成交)
|
||||
update_fields['jiesuan_dingdan_shu'] = F('jiesuan_dingdan_shu') + 1
|
||||
update_fields['jiesuan_jine'] = F('jiesuan_jine') + amount
|
||||
update_fields['SettledOrderCount'] = F('SettledOrderCount') + 1
|
||||
update_fields['SettledAmount'] = F('SettledAmount') + amount
|
||||
elif action == 3: # 退款
|
||||
update_fields['tuikuan_dingdan_shu'] = F('tuikuan_dingdan_shu') + 1
|
||||
update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount
|
||||
update_fields['RefundOrderCount'] = F('RefundOrderCount') + 1
|
||||
update_fields['RefundAmount'] = F('RefundAmount') + amount
|
||||
else:
|
||||
# 无效的行为类型,直接返回
|
||||
return
|
||||
|
||||
# 执行原子更新
|
||||
ShangjiaRiTongji.query.filter(id=stat.id).update(**update_fields)
|
||||
MerchantDailyStats.query.filter(id=stat.id).update(**update_fields)
|
||||
|
||||
if order_id:
|
||||
try:
|
||||
from merchant_ops.services.stats import sync_staff_stats_by_order_id
|
||||
sync_staff_stats_by_order_id(order_id, amount, action)
|
||||
except Exception:
|
||||
logger.warning('同步客服日统计失败 order_id=%s', order_id, exc_info=True)
|
||||
|
||||
|
||||
|
||||
def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
|
||||
def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00'), club_id=None):
|
||||
"""
|
||||
根据操作行为更新管事每日统计(邀请打手、充值会员、订单/押金分红)
|
||||
action: 1 = 邀请打手, 2 = 充值会员, 3 = 商家订单分红, 4 = 押金分红
|
||||
"""
|
||||
cid = _club_id_for_yonghuid(yonghuid, club_id)
|
||||
today = date.today()
|
||||
|
||||
with transaction.atomic():
|
||||
stat, created = GuanshiRiTongji.objects.select_for_update().get_or_create(
|
||||
yonghuid=yonghuid,
|
||||
riqi=today,
|
||||
stat, created = ManagerDailyStats.objects.select_for_update().get_or_create(
|
||||
club_id=cid,
|
||||
ManagerID=yonghuid,
|
||||
Date=today,
|
||||
defaults={
|
||||
'yonghuid': yonghuid,
|
||||
'riqi': today,
|
||||
'yaoqing_dashou_shu': 0,
|
||||
'chongzhi_dashou_shu': 0,
|
||||
'shouru_zonge': Decimal('0.00'),
|
||||
'club_id': cid,
|
||||
'ManagerID': yonghuid,
|
||||
'Date': today,
|
||||
'InvitedPlayerCount': 0,
|
||||
'RechargedPlayerCount': 0,
|
||||
'TotalIncome': Decimal('0.00'),
|
||||
}
|
||||
)
|
||||
|
||||
if action == 1:
|
||||
# 邀请打手:无论新创建还是已存在,直接 +1
|
||||
if created:
|
||||
stat.yaoqing_dashou_shu = 1
|
||||
stat.InvitedPlayerCount = 1
|
||||
stat.save()
|
||||
else:
|
||||
GuanshiRiTongji.query.filter(id=stat.id).update(
|
||||
yaoqing_dashou_shu=F('yaoqing_dashou_shu') + 1
|
||||
ManagerDailyStats.query.filter(id=stat.id).update(
|
||||
InvitedPlayerCount=F('InvitedPlayerCount') + 1
|
||||
)
|
||||
|
||||
elif action == 2:
|
||||
@@ -253,13 +371,13 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
|
||||
if amount <= 0:
|
||||
return
|
||||
if created:
|
||||
stat.chongzhi_dashou_shu = 1
|
||||
stat.shouru_zonge = amount
|
||||
stat.RechargedPlayerCount = 1
|
||||
stat.TotalIncome = amount
|
||||
stat.save()
|
||||
else:
|
||||
GuanshiRiTongji.query.filter(id=stat.id).update(
|
||||
chongzhi_dashou_shu=F('chongzhi_dashou_shu') + 1,
|
||||
shouru_zonge=F('shouru_zonge') + amount
|
||||
ManagerDailyStats.query.filter(id=stat.id).update(
|
||||
RechargedPlayerCount=F('RechargedPlayerCount') + 1,
|
||||
TotalIncome=F('TotalIncome') + amount
|
||||
)
|
||||
|
||||
elif action in (3, 4):
|
||||
@@ -267,161 +385,97 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
|
||||
if amount <= 0:
|
||||
return
|
||||
if created:
|
||||
stat.shouru_zonge = amount
|
||||
stat.TotalIncome = amount
|
||||
stat.save()
|
||||
else:
|
||||
GuanshiRiTongji.query.filter(id=stat.id).update(
|
||||
shouru_zonge=F('shouru_zonge') + amount
|
||||
ManagerDailyStats.query.filter(id=stat.id).update(
|
||||
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')):
|
||||
"""
|
||||
管事续费统计(单独表)
|
||||
用法: update_guanshi_xufei_daily('300001', Decimal('30'))
|
||||
"""
|
||||
def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00'), club_id=None):
|
||||
"""管事续费统计(单独表)"""
|
||||
cid = _club_id_for_yonghuid(yonghuid, club_id)
|
||||
today = date.today()
|
||||
with transaction.atomic():
|
||||
stat, created = GuanshiXufeiRiTongji.objects.select_for_update().get_or_create(
|
||||
yonghuid=yonghuid,
|
||||
riqi=today,
|
||||
stat, created = ManagerRenewalDailyStats.objects.select_for_update().get_or_create(
|
||||
club_id=cid,
|
||||
ManagerID=yonghuid,
|
||||
Date=today,
|
||||
defaults={
|
||||
'yonghuid': yonghuid,
|
||||
'riqi': today,
|
||||
'xufei_zongshu': 1,
|
||||
'xufei_shouyi': xufei_jine,
|
||||
'club_id': cid,
|
||||
'ManagerID': yonghuid,
|
||||
'Date': today,
|
||||
'RenewalTotal': 1,
|
||||
'RenewalRevenue': xufei_jine,
|
||||
}
|
||||
)
|
||||
if not created:
|
||||
GuanshiXufeiRiTongji.query.filter(id=stat.id).update(
|
||||
xufei_zongshu=F('xufei_zongshu') + 1,
|
||||
xufei_shouyi=F('xufei_shouyi') + xufei_jine
|
||||
ManagerRenewalDailyStats.query.filter(id=stat.id).update(
|
||||
RenewalTotal=F('RenewalTotal') + 1,
|
||||
RenewalRevenue=F('RenewalRevenue') + xufei_jine
|
||||
)
|
||||
|
||||
|
||||
def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
|
||||
"""
|
||||
根据操作行为更新组长每日统计(邀请管事、分佣收入)
|
||||
|
||||
参数:
|
||||
yonghuid: 组长用户ID
|
||||
action: 操作类型
|
||||
1 = 邀请管事(次数+1,无金额)
|
||||
2 = 分佣收入(累加金额)
|
||||
amount: 分佣金额(Decimal),仅在 action=2 时有效
|
||||
"""
|
||||
def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00'), club_id=None):
|
||||
"""根据操作行为更新组长每日统计"""
|
||||
cid = _club_id_for_yonghuid(yonghuid, club_id)
|
||||
today = date.today()
|
||||
|
||||
with transaction.atomic():
|
||||
stat, created = ZuzhangRiTongji.objects.select_for_update().get_or_create(
|
||||
yonghuid=yonghuid,
|
||||
riqi=today,
|
||||
stat, created = LeaderDailyStats.objects.select_for_update().get_or_create(
|
||||
club_id=cid,
|
||||
LeaderID=yonghuid,
|
||||
Date=today,
|
||||
defaults={
|
||||
'yonghuid': yonghuid,
|
||||
'riqi': today,
|
||||
'yaoqing_guanshi_shu': 0,
|
||||
'shouru_zonge': Decimal('0.00'),
|
||||
'fenyong_jine': Decimal('0.00'),
|
||||
'club_id': cid,
|
||||
'LeaderID': yonghuid,
|
||||
'Date': today,
|
||||
'InvitedManagerCount': 0,
|
||||
'TotalIncome': Decimal('0.00'),
|
||||
'CommissionAmount': Decimal('0.00'),
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
# 新记录直接设置初始值
|
||||
if action == 1:
|
||||
stat.yaoqing_guanshi_shu = 1
|
||||
stat.InvitedManagerCount = 1
|
||||
elif action == 2:
|
||||
amount = Decimal(str(amount)) if amount else Decimal('0.00')
|
||||
stat.shouru_zonge = amount
|
||||
stat.fenyong_jine = amount
|
||||
stat.TotalIncome = amount
|
||||
stat.CommissionAmount = amount
|
||||
stat.save()
|
||||
else:
|
||||
# 已存在记录,使用 F 表达式原子累加
|
||||
if action == 1:
|
||||
ZuzhangRiTongji.query.filter(id=stat.id).update(
|
||||
yaoqing_guanshi_shu=F('yaoqing_guanshi_shu') + 1
|
||||
LeaderDailyStats.query.filter(id=stat.id).update(
|
||||
InvitedManagerCount=F('InvitedManagerCount') + 1
|
||||
)
|
||||
elif action == 2:
|
||||
amount = Decimal(str(amount)) if amount else Decimal('0.00')
|
||||
if amount > 0:
|
||||
ZuzhangRiTongji.query.filter(id=stat.id).update(
|
||||
shouru_zonge=F('shouru_zonge') + amount,
|
||||
fenyong_jine=F('fenyong_jine') + amount
|
||||
LeaderDailyStats.query.filter(id=stat.id).update(
|
||||
TotalIncome=F('TotalIncome') + amount,
|
||||
CommissionAmount=F('CommissionAmount') + amount
|
||||
)
|
||||
|
||||
|
||||
|
||||
def update_tixian_daily_stat(leixing, amount):
|
||||
def update_tixian_daily_stat(leixing, amount, club_id=None):
|
||||
"""
|
||||
更新每日提现统计(平台维度)
|
||||
更新每日提现统计(平台维度,按俱乐部)
|
||||
leixing: 1=打手, 2=管事, 3=组长
|
||||
amount: 本次提现金额(申请金额,含手续费)
|
||||
"""
|
||||
from jituan.constants import CLUB_ID_DEFAULT
|
||||
from jituan.services.withdraw_config import get_or_create_platform_daily_stat
|
||||
|
||||
club_id = (club_id or '').strip() or CLUB_ID_DEFAULT
|
||||
today = date.today()
|
||||
with transaction.atomic():
|
||||
stat, created = TixianRiTongji.objects.select_for_update().get_or_create(
|
||||
riqi=today,
|
||||
leixing=leixing,
|
||||
defaults={'total_amount': amount}
|
||||
)
|
||||
if not created:
|
||||
TixianRiTongji.query.filter(id=stat.id).update(
|
||||
stat = get_or_create_platform_daily_stat(club_id, leixing, today)
|
||||
WithdrawalDailyStats.query.filter(id=stat.id).update(
|
||||
total_amount=F('total_amount') + amount
|
||||
)
|
||||
|
||||
@@ -447,10 +501,10 @@ _SHENFEN_KEYWORDS = {
|
||||
}
|
||||
|
||||
SHENFEN_PROFILE_MAP = {
|
||||
1: 'dashou_profile',
|
||||
2: 'guanshi_profile',
|
||||
3: 'shop_profile',
|
||||
4: 'zuzhang_profile',
|
||||
1: 'DashouProfile',
|
||||
2: 'GuanshiProfile',
|
||||
3: 'ShopProfile',
|
||||
4: 'ZuzhangProfile',
|
||||
}
|
||||
|
||||
|
||||
@@ -514,3 +568,91 @@ def check_fadan_permission(permissions, shenfen):
|
||||
if sf == shenfen and perm in permissions:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def fmt_datetime(dt):
|
||||
"""格式化时间为前端通用字符串。"""
|
||||
if not dt:
|
||||
return ''
|
||||
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
|
||||
def datetime_aliases(dt):
|
||||
"""同时返回 CreateTime / create_time,避免前后端字段名不一致。"""
|
||||
s = fmt_datetime(dt)
|
||||
return {'CreateTime': s, 'create_time': s}
|
||||
|
||||
|
||||
def pick_order_id(data):
|
||||
"""兼容前端 dingdan_id 与 SDK 字段 OrderID。"""
|
||||
return str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
|
||||
|
||||
|
||||
def pick_leixing_id(data):
|
||||
"""兼容前端 leixing_id 与 SDK 字段 ProductTypeID。"""
|
||||
val = data.get('leixing_id')
|
||||
if val is None or val == '':
|
||||
val = data.get('ProductTypeID')
|
||||
if val is None or val == '':
|
||||
return None
|
||||
try:
|
||||
return int(val)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def pick_penalty_create_params(data):
|
||||
"""兼容罚单创建:SDK 字段名与旧版拼音字段名。"""
|
||||
penalized_id = str(data.get('PenalizedUserID') or data.get('beichufa_id') or '').strip()
|
||||
reason = str(data.get('Reason') or data.get('chufaliyou') or '').strip()
|
||||
fine_amount = data.get('FineAmount')
|
||||
if fine_amount is None:
|
||||
fine_amount = data.get('fakuanjine')
|
||||
affects = data.get('AffectsGrabbing')
|
||||
if affects is None:
|
||||
affects = data.get('yingxiang_qiangdan', 1)
|
||||
return penalized_id, reason, fine_amount, affects
|
||||
|
||||
|
||||
# ---------- 修改记录(Xiugaijilu) ----------
|
||||
XIUGAI_LEIXING_DASHOU = 2
|
||||
XIUGAI_LEIXING_GUANSHI = 3
|
||||
XIUGAI_LEIXING_SHANGJIA = 4
|
||||
XIUGAI_LEIXING_ZUZHANG = 5
|
||||
|
||||
XIUGAI_LEIXING_LABEL = {
|
||||
XIUGAI_LEIXING_DASHOU: '打手',
|
||||
XIUGAI_LEIXING_GUANSHI: '管事',
|
||||
XIUGAI_LEIXING_SHANGJIA: '商家',
|
||||
XIUGAI_LEIXING_ZUZHANG: '组长',
|
||||
}
|
||||
|
||||
|
||||
def write_xiugai_log(*, yonghuid, xiugaiid, leixing, qitashuoming, club_id=None, request_ip='', **extra_fields):
|
||||
"""
|
||||
统一写入 xiugaijilu 修改记录。
|
||||
qitashuoming 应详细描述本次操作(尤其是金额、身份、上下级变更等)。
|
||||
extra_fields 可传入 xiugaitijiao、shangjiayue、guanshiyue 等模型字段。
|
||||
"""
|
||||
from users.models import Xiugaijilu
|
||||
from jituan.services.admin_audit import log_admin_audit_from_xiugai
|
||||
|
||||
if not qitashuoming:
|
||||
label = XIUGAI_LEIXING_LABEL.get(leixing, f'类型{leixing}')
|
||||
qitashuoming = f'【后台】修改{label}信息,操作人{xiugaiid}'
|
||||
|
||||
Xiugaijilu.query.create(
|
||||
yonghuid=str(yonghuid),
|
||||
xiugaiid=str(xiugaiid),
|
||||
leixing=leixing,
|
||||
qitashuoming=qitashuoming,
|
||||
**extra_fields,
|
||||
)
|
||||
log_admin_audit_from_xiugai(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=xiugaiid,
|
||||
leixing=leixing,
|
||||
qitashuoming=qitashuoming,
|
||||
club_id=club_id,
|
||||
request_ip=request_ip,
|
||||
)
|
||||
8483
backend/view.py
8483
backend/view.py
File diff suppressed because it is too large
Load Diff
0
backend/views/__init__.py
Normal file
0
backend/views/__init__.py
Normal file
754
backend/views/exams.py
Normal file
754
backend/views/exams.py
Normal file
@@ -0,0 +1,754 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
# 审核记录状态映射(原位于 zxkf.py,按功能组归类到考核模块)
|
||||
SHENHE_ZHUANGTAI_MAP = {
|
||||
0: '考核中',
|
||||
1: '已通过',
|
||||
2: '未通过',
|
||||
3: '待审核',
|
||||
}
|
||||
|
||||
class KhgglView(APIView):
|
||||
"""
|
||||
考核官管理数据接口(列表、筛选、分页)
|
||||
POST /houtai/khggl
|
||||
Body: {
|
||||
"phone": "管理员手机号",
|
||||
"page": 1, // 可选,默认1
|
||||
"page_size": 15, // 可选,默认15
|
||||
"yonghuid": "1234567", // 按用户ID筛选,可选
|
||||
"zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选
|
||||
"shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选
|
||||
"bankuai_name": "王者" // 板块名称模糊筛选,可选
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
|
||||
if 'kaohepeizhi' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
|
||||
|
||||
# 分页参数
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 15))
|
||||
if page < 1: page = 1
|
||||
if page_size < 1: page_size = 15
|
||||
|
||||
# 筛选条件
|
||||
filter_yonghuid = request.data.get('yonghuid', '').strip()
|
||||
filter_zhuangtai = request.data.get('zhuangtai')
|
||||
filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai')
|
||||
filter_bankuai_name = request.data.get('bankuai_name', '').strip()
|
||||
|
||||
# 基础查询:所有审核官,预加载用户及打手扩展表
|
||||
queryset = filter_user_related_by_club(
|
||||
UserShenheguan.query.select_related('user__DashouProfile'),
|
||||
request,
|
||||
)
|
||||
|
||||
if filter_yonghuid:
|
||||
queryset = queryset.filter(user__UserUID=filter_yonghuid)
|
||||
if filter_zhuangtai is not None:
|
||||
queryset = queryset.filter(zhuangtai=filter_zhuangtai)
|
||||
if filter_shenhe_zhuangtai is not None:
|
||||
queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai)
|
||||
|
||||
# 如果填写了板块名称,筛选出关联该板块的审核官
|
||||
if filter_bankuai_name:
|
||||
bankuai_ids = Bankuai.query.filter(
|
||||
mingcheng__icontains=filter_bankuai_name
|
||||
).values_list('bankuai_id', flat=True)
|
||||
shenheguan_ids = KaoheguanBankuai.query.filter(
|
||||
bankuai_id__in=bankuai_ids
|
||||
).values_list('kaoheguan_id', flat=True)
|
||||
queryset = queryset.filter(user__UserUID__in=shenheguan_ids)
|
||||
|
||||
total = queryset.count()
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
|
||||
|
||||
from collections import defaultdict
|
||||
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
|
||||
|
||||
# ---------- 优化:板块打手数量(1 次 annotate 替代 2N 次循环查询)----------
|
||||
# 原逻辑:每板块查 dashou 标签 ID,再统计关联的不同用户数(要求有 DashouProfile)
|
||||
bankuai_dashou_qs = yonghu_chenghao_qs_for_request(
|
||||
YonghuChenghao.query.filter(
|
||||
chenghao__leixing='dashou',
|
||||
yonghu__DashouProfile__isnull=False,
|
||||
),
|
||||
request,
|
||||
)
|
||||
bankuai_dashou_count_map = {
|
||||
item['chenghao__bankuai_id']: item['user_count']
|
||||
for item in bankuai_dashou_qs.values('chenghao__bankuai_id').annotate(
|
||||
user_count=Count('yonghu', distinct=True)
|
||||
)
|
||||
}
|
||||
|
||||
# ---------- 优化:板块标签统计(1 次 annotate 替代 N*M 次循环查询)----------
|
||||
# 原逻辑:每板块每标签分别统计关联的不同用户数(不要求 DashouProfile)
|
||||
tag_counts_qs = yonghu_chenghao_qs_for_request(
|
||||
YonghuChenghao.query.filter(chenghao__leixing='dashou'),
|
||||
request,
|
||||
).values(
|
||||
'chenghao_id', 'chenghao__bankuai_id', 'chenghao__mingcheng'
|
||||
).annotate(user_count=Count('yonghu', distinct=True))
|
||||
|
||||
tags_by_bankuai = defaultdict(list)
|
||||
for item in tag_counts_qs:
|
||||
tags_by_bankuai[item['chenghao__bankuai_id']].append({
|
||||
'id': item['chenghao_id'],
|
||||
'name': item['chenghao__mingcheng'],
|
||||
'user_count': item['user_count'],
|
||||
})
|
||||
|
||||
all_banquai = Bankuai.query.all()
|
||||
bankuai_tags_data = [
|
||||
{
|
||||
'bankuai_id': bk.bankuai_id,
|
||||
'mingcheng': bk.mingcheng,
|
||||
'tags': tags_by_bankuai.get(bk.bankuai_id, []),
|
||||
}
|
||||
for bk in all_banquai
|
||||
]
|
||||
|
||||
# ---------- 优化:审核官板块关联(1 次批量查询替代 P 次循环查询)----------
|
||||
shenheguan_user_uids = [sg.user.UserUID for sg in shenheguan_list]
|
||||
all_kgs = KaoheguanBankuai.query.filter(
|
||||
kaoheguan_id__in=shenheguan_user_uids
|
||||
).select_related('bankuai')
|
||||
kg_map = defaultdict(list)
|
||||
for kg in all_kgs:
|
||||
kg_map[kg.kaoheguan_id].append(kg)
|
||||
|
||||
# 构造返回数据(审核官列表)
|
||||
data_list = []
|
||||
for sg in shenheguan_list:
|
||||
user = sg.user
|
||||
bankuai_info = [
|
||||
{
|
||||
'bankuai_id': kg.bankuai.bankuai_id,
|
||||
'mingcheng': kg.bankuai.mingcheng,
|
||||
'dashou_count': bankuai_dashou_count_map.get(kg.bankuai.bankuai_id, 0),
|
||||
}
|
||||
for kg in kg_map.get(user.UserUID, [])
|
||||
]
|
||||
|
||||
dashou_nick = user.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else ''
|
||||
|
||||
data_list.append({
|
||||
'yonghuid': user.UserUID,
|
||||
'avatar': user.Avatar or '',
|
||||
'phone': user.Phone or '',
|
||||
'zhuangtai': sg.zhuangtai,
|
||||
'shenhe_zhuangtai': sg.shenhe_zhuangtai,
|
||||
'shenhe_zongshu': sg.shenhe_zongshu,
|
||||
'tongguo_zongshu': sg.tongguo_zongshu,
|
||||
'yue': float(sg.yue),
|
||||
'zonge': float(sg.zonge),
|
||||
'CreateTime': sg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.CreateTime else '',
|
||||
'UpdateTime': sg.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.UpdateTime else '',
|
||||
'bankuai': bankuai_info,
|
||||
'nicheng': dashou_nick,
|
||||
'is_renzheng': sg.is_renzheng,
|
||||
})
|
||||
|
||||
all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'list': data_list,
|
||||
'all_bankuai': all_bankuai,
|
||||
'bankuai_tags': bankuai_tags_data, # 新增板块标签统计
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("KhgglView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
class ShgxgsjView(APIView):
|
||||
"""
|
||||
审核官信息修改(余额、状态、板块关联)
|
||||
POST /houtai/shgxgsj
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'kaohepeizhi' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
|
||||
|
||||
yonghuid = request.data.get('yonghuid')
|
||||
if not yonghuid:
|
||||
return Response({'code': 1, 'msg': '缺少用户ID'}, status=400)
|
||||
|
||||
try:
|
||||
shenheguan = UserShenheguan.query.select_related('user').get(user__UserUID=yonghuid)
|
||||
except UserShenheguan.DoesNotExist:
|
||||
return Response({'code': 1, 'msg': '审核官不存在'}, status=404)
|
||||
|
||||
from jituan.services.shenhe_club import user_belongs_to_request_club
|
||||
if not user_belongs_to_request_club(shenheguan.user, request):
|
||||
return Response({'code': 403, 'msg': '该考核官不属于当前俱乐部'}, status=403)
|
||||
|
||||
action = request.data.get('action')
|
||||
|
||||
if action == 'update_info':
|
||||
if 'yue' in request.data:
|
||||
shenheguan.yue = float(request.data.get('yue', 0))
|
||||
if 'zhuangtai' in request.data:
|
||||
zt = int(request.data.get('zhuangtai'))
|
||||
if zt not in [0, 1]:
|
||||
return Response({'code': 1, 'msg': '无效的账号状态'}, status=400)
|
||||
shenheguan.zhuangtai = zt
|
||||
if 'shenhe_zhuangtai' in request.data:
|
||||
szt = int(request.data.get('shenhe_zhuangtai'))
|
||||
if szt not in [0, 1]:
|
||||
return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400)
|
||||
shenheguan.shenhe_zhuangtai = szt
|
||||
# 新增认证状态编辑
|
||||
if 'is_renzheng' in request.data:
|
||||
shenheguan.is_renzheng = bool(request.data.get('is_renzheng'))
|
||||
shenheguan.save()
|
||||
return Response({'code': 0, 'msg': '修改成功'})
|
||||
|
||||
elif action == 'add_bankuai':
|
||||
bankuai_id = request.data.get('bankuai_id')
|
||||
if not bankuai_id:
|
||||
return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
|
||||
if not Bankuai.query.filter(bankuai_id=bankuai_id).exists():
|
||||
return Response({'code': 1, 'msg': '板块不存在'}, status=400)
|
||||
if KaoheguanBankuai.query.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists():
|
||||
return Response({'code': 1, 'msg': '已关联该板块'}, status=400)
|
||||
KaoheguanBankuai.query.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id)
|
||||
return Response({'code': 0, 'msg': '板块关联成功'})
|
||||
|
||||
elif action == 'remove_bankuai':
|
||||
bankuai_id = request.data.get('bankuai_id')
|
||||
if not bankuai_id:
|
||||
return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
|
||||
deleted, _ = KaoheguanBankuai.query.filter(
|
||||
kaoheguan_id=yonghuid, bankuai_id=bankuai_id
|
||||
).delete()
|
||||
if not deleted:
|
||||
return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400)
|
||||
return Response({'code': 0, 'msg': '板块移除成功'})
|
||||
|
||||
else:
|
||||
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("ShgxgsjView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _batch_user_profile_map(user_uids):
|
||||
"""批量获取用户头像、昵称、手机号"""
|
||||
uids = list({u for u in user_uids if u})
|
||||
if not uids:
|
||||
return {}
|
||||
users = User.query.filter(UserUID__in=uids)
|
||||
dashou_nicks = {
|
||||
d.user.UserUID: d.nicheng
|
||||
for d in UserDashou.query.filter(user__UserUID__in=uids).select_related('user')
|
||||
}
|
||||
result = {}
|
||||
for u in users:
|
||||
result[u.UserUID] = {
|
||||
'yonghuid': u.UserUID,
|
||||
'avatar': u.Avatar or '',
|
||||
'phone': u.Phone or '',
|
||||
'nicheng': dashou_nicks.get(u.UserUID) or u.UserName or u.Phone or u.UserUID,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _build_shenhe_jilu_filters(request_data):
|
||||
"""构造 ShenheJilu 查询条件(列表与统计共用)"""
|
||||
qs = ShenheJilu.query.select_related('bankuai', 'chenghao')
|
||||
|
||||
jilu_id = (request_data.get('jilu_id') or '').strip()
|
||||
if jilu_id:
|
||||
qs = qs.filter(jilu_id=jilu_id)
|
||||
|
||||
zhuangtai = request_data.get('zhuangtai')
|
||||
if zhuangtai is not None and zhuangtai != '':
|
||||
qs = qs.filter(zhuangtai=int(zhuangtai))
|
||||
|
||||
assessed = request_data.get('assessed')
|
||||
if assessed == 'done':
|
||||
qs = qs.filter(zhuangtai__in=[1, 2])
|
||||
elif assessed == 'undone':
|
||||
qs = qs.filter(zhuangtai__in=[0, 3])
|
||||
|
||||
bankuai_id = request_data.get('bankuai_id')
|
||||
if bankuai_id not in (None, ''):
|
||||
qs = qs.filter(bankuai_id=int(bankuai_id))
|
||||
|
||||
chenghao_id = request_data.get('chenghao_id')
|
||||
if chenghao_id not in (None, ''):
|
||||
qs = qs.filter(chenghao_id=int(chenghao_id))
|
||||
|
||||
cishu = request_data.get('cishu')
|
||||
if cishu not in (None, ''):
|
||||
qs = qs.filter(cishu=int(cishu))
|
||||
|
||||
dashou_kw = (request_data.get('dashou_keyword') or '').strip()
|
||||
if dashou_kw:
|
||||
dashou_uids = list(
|
||||
User.query.filter(
|
||||
Q(UserUID=dashou_kw) |
|
||||
Q(UserName__icontains=dashou_kw) |
|
||||
Q(Phone__icontains=dashou_kw) |
|
||||
Q(DashouProfile__nicheng__icontains=dashou_kw)
|
||||
).values_list('UserUID', flat=True)
|
||||
)
|
||||
if dashou_uids:
|
||||
qs = qs.filter(shenqingren_id__in=dashou_uids)
|
||||
else:
|
||||
qs = qs.none()
|
||||
|
||||
kg_kw = (request_data.get('kaoheguan_keyword') or '').strip()
|
||||
if kg_kw:
|
||||
kg_uids = list(
|
||||
User.query.filter(
|
||||
Q(UserUID=kg_kw) |
|
||||
Q(UserName__icontains=kg_kw) |
|
||||
Q(Phone__icontains=kg_kw) |
|
||||
Q(DashouProfile__nicheng__icontains=kg_kw)
|
||||
).values_list('UserUID', flat=True)
|
||||
)
|
||||
if kg_uids:
|
||||
qs = qs.filter(shenheguan_id__in=kg_uids)
|
||||
else:
|
||||
qs = qs.none()
|
||||
|
||||
start_time = (request_data.get('start_time') or '').strip()
|
||||
end_time = (request_data.get('end_time') or '').strip()
|
||||
if start_time:
|
||||
qs = qs.filter(CreateTime__gte=start_time)
|
||||
if end_time:
|
||||
qs = qs.filter(CreateTime__lte=end_time)
|
||||
|
||||
has_kaoheguan = request_data.get('has_kaoheguan')
|
||||
if has_kaoheguan in (True, 'true', '1', 1):
|
||||
qs = qs.exclude(shenheguan_id__isnull=True).exclude(shenheguan_id='')
|
||||
elif has_kaoheguan in (False, 'false', '0', 0):
|
||||
qs = qs.filter(Q(shenheguan_id__isnull=True) | Q(shenheguan_id=''))
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
def _serialize_shenhe_jilu_row(jl, profile_map, fee_map=None, reject_count_map=None):
|
||||
dashou = profile_map.get(jl.shenqingren_id, {
|
||||
'yonghuid': jl.shenqingren_id,
|
||||
'avatar': '',
|
||||
'phone': '',
|
||||
'nicheng': jl.shenqingren_id,
|
||||
})
|
||||
kaoheguan = None
|
||||
if jl.shenheguan_id:
|
||||
kaoheguan = profile_map.get(jl.shenheguan_id, {
|
||||
'yonghuid': jl.shenheguan_id,
|
||||
'avatar': '',
|
||||
'phone': '',
|
||||
'nicheng': jl.shenheguan_id,
|
||||
})
|
||||
kaoheguan['game_id'] = jl.shenheguan_youxi_id or ''
|
||||
|
||||
current_fee = 0.0
|
||||
if fee_map is not None:
|
||||
current_fee = fee_map.get((jl.jilu_id, jl.cishu), 0.0)
|
||||
else:
|
||||
fd = KaoheJiluFeiyong.query.filter(jilu_id=jl.jilu_id, cishu=jl.cishu).first()
|
||||
if fd:
|
||||
current_fee = float(fd.jine)
|
||||
|
||||
reject_count = 0
|
||||
if reject_count_map is not None:
|
||||
reject_count = reject_count_map.get(jl.jilu_id, 0)
|
||||
|
||||
return {
|
||||
'jilu_id': jl.jilu_id,
|
||||
'zhuangtai': jl.zhuangtai,
|
||||
'zhuangtai_text': SHENHE_ZHUANGTAI_MAP.get(jl.zhuangtai, str(jl.zhuangtai)),
|
||||
'cishu': jl.cishu,
|
||||
'jiaofei_jine': float(jl.jiaofei_jine or 0),
|
||||
'current_fee': current_fee,
|
||||
'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None,
|
||||
'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '',
|
||||
'chenghao_id': jl.chenghao.id if jl.chenghao else None,
|
||||
'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '',
|
||||
'chenghao_icon': jl.chenghao.tubiao_url if jl.chenghao else '',
|
||||
'guize_neirong': jl.guize_neirong or '',
|
||||
'dashou': {
|
||||
**dashou,
|
||||
'game_id': jl.dashou_youxi_id or '',
|
||||
'beizhu': jl.dashou_beizhu or '',
|
||||
},
|
||||
'kaoheguan': kaoheguan,
|
||||
'reject_count': reject_count,
|
||||
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
|
||||
'UpdateTime': jl.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.UpdateTime else '',
|
||||
}
|
||||
|
||||
|
||||
class KhjlglView(APIView):
|
||||
"""
|
||||
考核记录列表 + 统计
|
||||
POST /houtai/khjlgl
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'kaohepeizhi' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
|
||||
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 20))
|
||||
if page < 1:
|
||||
page = 1
|
||||
if page_size < 1:
|
||||
page_size = 20
|
||||
if page_size > 100:
|
||||
page_size = 100
|
||||
|
||||
from jituan.services.shenhe_club import filter_shenhe_jilu_by_request
|
||||
|
||||
filter_data = request.data
|
||||
qs = filter_shenhe_jilu_by_request(
|
||||
_build_shenhe_jilu_filters(filter_data),
|
||||
request,
|
||||
)
|
||||
|
||||
# FluentQuery.filter() 会原地修改同一对象;统计与列表必须各自独立查询
|
||||
stats_base = filter_shenhe_jilu_by_request(
|
||||
_build_shenhe_jilu_filters(filter_data),
|
||||
request,
|
||||
)
|
||||
agg = stats_base.aggregate(total_fee=Sum('jiaofei_jine'))
|
||||
tag_rows = filter_shenhe_jilu_by_request(
|
||||
_build_shenhe_jilu_filters(filter_data),
|
||||
request,
|
||||
).values(
|
||||
'chenghao_id', 'chenghao__mingcheng'
|
||||
).annotate(
|
||||
total=Count('jilu_id'),
|
||||
passed=Count('jilu_id', filter=Q(zhuangtai=1)),
|
||||
failed=Count('jilu_id', filter=Q(zhuangtai=2)),
|
||||
in_progress=Count('jilu_id', filter=Q(zhuangtai=0)),
|
||||
pending=Count('jilu_id', filter=Q(zhuangtai=3)),
|
||||
).order_by('-total')
|
||||
|
||||
_stats_agg = stats_base.aggregate(
|
||||
total=Count('id'),
|
||||
passed=Count('id', filter=Q(zhuangtai=1)),
|
||||
failed=Count('id', filter=Q(zhuangtai=2)),
|
||||
in_progress=Count('id', filter=Q(zhuangtai=0)),
|
||||
pending=Count('id', filter=Q(zhuangtai=3)),
|
||||
)
|
||||
stats = {
|
||||
'total': _stats_agg['total'],
|
||||
'passed': _stats_agg['passed'],
|
||||
'failed': _stats_agg['failed'],
|
||||
'in_progress': _stats_agg['in_progress'],
|
||||
'pending': _stats_agg['pending'],
|
||||
'total_fee': float(agg['total_fee'] or 0),
|
||||
'tag_stats': [
|
||||
{
|
||||
'chenghao_id': r['chenghao_id'],
|
||||
'chenghao_name': r['chenghao__mingcheng'] or '未知标签',
|
||||
'total': r['total'],
|
||||
'passed': r['passed'],
|
||||
'failed': r['failed'],
|
||||
'in_progress': r['in_progress'],
|
||||
'pending': r['pending'],
|
||||
}
|
||||
for r in tag_rows if r['chenghao_id']
|
||||
],
|
||||
}
|
||||
|
||||
total = qs.count()
|
||||
start = (page - 1) * page_size
|
||||
rows = qs.order_by('-CreateTime')[start:start + page_size].to_list()
|
||||
|
||||
uids = []
|
||||
jilu_ids = []
|
||||
for jl in rows:
|
||||
uids.append(jl.shenqingren_id)
|
||||
if jl.shenheguan_id:
|
||||
uids.append(jl.shenheguan_id)
|
||||
jilu_ids.append(jl.jilu_id)
|
||||
profile_map = _batch_user_profile_map(uids)
|
||||
|
||||
fee_pairs = [(jl.jilu_id, jl.cishu) for jl in rows]
|
||||
fee_map = {}
|
||||
if fee_pairs:
|
||||
jids = [p[0] for p in fee_pairs]
|
||||
for fd in KaoheJiluFeiyong.query.filter(jilu_id__in=jids):
|
||||
fee_map[(fd.jilu_id, fd.cishu)] = float(fd.jine)
|
||||
|
||||
reject_counts = {
|
||||
r['jilu_id']: r['cnt']
|
||||
for r in KaoheJujueJilu.query.filter(jilu_id__in=jilu_ids)
|
||||
.values('jilu_id')
|
||||
.annotate(cnt=Count('id'))
|
||||
.all()
|
||||
} if jilu_ids else {}
|
||||
|
||||
data_list = [
|
||||
_serialize_shenhe_jilu_row(jl, profile_map, fee_map, reject_counts)
|
||||
for jl in rows
|
||||
]
|
||||
|
||||
all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
|
||||
all_tags = list(
|
||||
Chenghao.query.filter(leixing='dashou')
|
||||
.values('id', 'mingcheng', 'bankuai_id')
|
||||
.order_by('mingcheng')
|
||||
)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'list': data_list,
|
||||
'stats': stats,
|
||||
'all_bankuai': all_bankuai,
|
||||
'all_tags': all_tags,
|
||||
},
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception('KhjlglView 接口错误')
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
class KhjlczView(APIView):
|
||||
"""
|
||||
考核记录操作 / 详情
|
||||
POST /houtai/khjlcz
|
||||
action:
|
||||
- get_detail: 获取单条详情(含缴费明细、拒绝历史)
|
||||
- to_pending: 考核中 → 待审核(取消指定考核官,与小程序 transfer 一致)
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'kaohepeizhi' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
|
||||
|
||||
action = request.data.get('action')
|
||||
jilu_id = (request.data.get('jilu_id') or '').strip()
|
||||
if not jilu_id:
|
||||
return Response({'code': 1, 'msg': '缺少记录ID'}, status=400)
|
||||
|
||||
if action == 'get_detail':
|
||||
try:
|
||||
jl = ShenheJilu.query.select_related('bankuai', 'chenghao').get(jilu_id=jilu_id)
|
||||
except ShenheJilu.DoesNotExist:
|
||||
return Response({'code': 1, 'msg': '记录不存在'}, status=404)
|
||||
|
||||
uids = [jl.shenqingren_id]
|
||||
if jl.shenheguan_id:
|
||||
uids.append(jl.shenheguan_id)
|
||||
profile_map = _batch_user_profile_map(uids)
|
||||
|
||||
fee_list = [
|
||||
{
|
||||
'cishu': f.cishu,
|
||||
'jine': float(f.jine),
|
||||
}
|
||||
for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu')
|
||||
]
|
||||
reject_list = []
|
||||
_reject_records = list(KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'))
|
||||
_kg_ids = [r.shenheguan_id for r in _reject_records if r.shenheguan_id]
|
||||
_kg_prof_map = _batch_user_profile_map(_kg_ids) if _kg_ids else {}
|
||||
for r in _reject_records:
|
||||
kg_prof = _kg_prof_map.get(r.shenheguan_id, {})
|
||||
reject_list.append({
|
||||
'cishu': r.cishu,
|
||||
'shenheguan_id': r.shenheguan_id,
|
||||
'shenheguan_nicheng': kg_prof.get('nicheng', r.shenheguan_id),
|
||||
'yuanyin': r.yuanyin,
|
||||
'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||||
})
|
||||
|
||||
record = _serialize_shenhe_jilu_row(jl, profile_map)
|
||||
record['fee_list'] = fee_list
|
||||
record['reject_list'] = reject_list
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': record})
|
||||
|
||||
if action == 'to_pending':
|
||||
with transaction.atomic():
|
||||
jl = ShenheJilu.objects.select_for_update().filter(jilu_id=jilu_id).first()
|
||||
if not jl:
|
||||
return Response({'code': 1, 'msg': '记录不存在'}, status=404)
|
||||
if jl.zhuangtai != 0:
|
||||
return Response({'code': 1, 'msg': '仅「考核中」的记录可退回待审核'}, status=400)
|
||||
|
||||
old_kg_id = jl.shenheguan_id
|
||||
jl.shenheguan_id = None
|
||||
jl.shenheguan_youxi_id = None
|
||||
jl.zhuangtai = 3
|
||||
jl.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id', 'zhuangtai', 'UpdateTime'])
|
||||
|
||||
if old_kg_id:
|
||||
UserShenheguan.query.filter(user__UserUID=old_kg_id).update(shenhe_zhuangtai=0)
|
||||
|
||||
return Response({'code': 0, 'msg': '已退回待审核,考核官指定已取消'})
|
||||
|
||||
return Response({'code': 1, 'msg': '无效的操作类型'}, status=400)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception('KhjlczView 接口错误')
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
983
backend/views/finance.py
Normal file
983
backend/views/finance.py
Normal file
@@ -0,0 +1,983 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
class CaiwuView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
|
||||
if 'caiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||||
|
||||
from jituan.services.caiwu_stats import build_caiwu_payload
|
||||
|
||||
try:
|
||||
data = build_caiwu_payload(request)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception:
|
||||
logger.exception('CaiwuView 异常')
|
||||
return Response({'code': 99, 'msg': '统计失败'}, status=500)
|
||||
|
||||
|
||||
class CwhybkhqView(APIView):
|
||||
"""
|
||||
获取所有板块及对应会员列表
|
||||
POST /houtai/cwhybkhq
|
||||
Body: { "phone": "管理员手机号" }
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
|
||||
if 'huiyuancaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
||||
|
||||
all_bankuai = list(Bankuai.query.all())
|
||||
bankuai_ids = [bk.bankuai_id for bk in all_bankuai]
|
||||
all_members = Huiyuan.query.filter(bankuai_id__in=bankuai_ids).values('huiyuan_id', 'jieshao', 'bankuai_id')
|
||||
members_by_bankuai = defaultdict(list)
|
||||
for m in all_members:
|
||||
members_by_bankuai[m['bankuai_id']].append(dict(m))
|
||||
bankuai_list = []
|
||||
for bk in all_bankuai:
|
||||
bankuai_list.append({
|
||||
'bankuai_id': bk.bankuai_id,
|
||||
'mingcheng': bk.mingcheng,
|
||||
'members': members_by_bankuai.get(bk.bankuai_id, [])
|
||||
})
|
||||
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list})
|
||||
except Exception as e:
|
||||
logger.exception("CwhybkhqView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
class HybkjtsjView(APIView):
|
||||
"""
|
||||
会员详细统计数据
|
||||
POST /houtai/hybkjtsj
|
||||
Body: {
|
||||
"phone": "管理员手机号",
|
||||
"huiyuan_id": "会员ID",
|
||||
"granularity": "day" | "month" | "year",
|
||||
"year": 2026,
|
||||
"month": 5,
|
||||
"include_guanshi": true,
|
||||
"include_zuzhang": true,
|
||||
"summary_date": "2026-05-16"
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
|
||||
if 'huiyuancaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
||||
|
||||
huiyuan_id = request.data.get('huiyuan_id')
|
||||
granularity = request.data.get('granularity', 'day')
|
||||
year = request.data.get('year')
|
||||
month = request.data.get('month')
|
||||
include_guanshi = request.data.get('include_guanshi', False)
|
||||
include_zuzhang = request.data.get('include_zuzhang', False)
|
||||
summary_date = request.data.get('summary_date')
|
||||
|
||||
if not huiyuan_id:
|
||||
return Response({'code': 1, 'msg': '缺少会员ID'}, status=400)
|
||||
|
||||
# 🔧 直接使用 date.today() 避免模块名冲突
|
||||
now = date.today()
|
||||
|
||||
# ---------- 构造时间范围 ----------
|
||||
if granularity == 'day':
|
||||
if not year or not month:
|
||||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||||
start_date = date(year, month, 1)
|
||||
if month == 12:
|
||||
end_date = date(year + 1, 1, 1)
|
||||
else:
|
||||
end_date = date(year, month + 1, 1)
|
||||
trunc_func = TruncDate('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = now.strftime('%Y-%m-%d')
|
||||
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||||
elif granularity == 'month':
|
||||
if not year:
|
||||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year + 1, 1, 1)
|
||||
trunc_func = TruncMonth('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = now.strftime('%Y-%m')
|
||||
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||||
elif granularity == 'year':
|
||||
year = year or now.year
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year + 1, 1, 1)
|
||||
trunc_func = TruncYear('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = str(year)
|
||||
summary_q = date(year, 1, 1)
|
||||
else:
|
||||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||||
|
||||
# 生成 naive datetime(避免 USE_TZ=False 报错)
|
||||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||||
|
||||
# ---------- 充值过滤 ----------
|
||||
chongzhi_filter = {
|
||||
'huiyuan_id': huiyuan_id, # 会员ID筛选
|
||||
'leixing': 1,
|
||||
'zhuangtai': 3,
|
||||
'CreateTime__gte': start_dt,
|
||||
'CreateTime__lt': end_dt
|
||||
}
|
||||
|
||||
# ---------- 分红过滤 ----------
|
||||
fenhong_filter = {
|
||||
'huiyuan_id': huiyuan_id, # 分红表会员ID筛选
|
||||
'CreateTime__gte': start_dt,
|
||||
'CreateTime__lt': end_dt
|
||||
}
|
||||
|
||||
# 充值分组查询
|
||||
chongzhi_qs = filter_queryset_by_club(
|
||||
Czjilu.query.filter(**chongzhi_filter), request,
|
||||
) \
|
||||
.annotate(period=trunc_func) \
|
||||
.values('period') \
|
||||
.annotate(
|
||||
chongzhi_count=Count('id'),
|
||||
chongzhi_amount=Sum('jine')
|
||||
).order_by('period')
|
||||
|
||||
# 分红分组查询
|
||||
fenhong_qs = None
|
||||
if include_guanshi or include_zuzhang:
|
||||
fenhong_agg = {}
|
||||
if include_guanshi:
|
||||
fenhong_agg['guanshi_amount'] = Sum('fenhong')
|
||||
if include_zuzhang:
|
||||
fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
|
||||
fenhong_qs = filter_gsfenhong_qs(
|
||||
Gsfenhong.query.filter(**fenhong_filter), request,
|
||||
) \
|
||||
.annotate(period=trunc_func) \
|
||||
.values('period') \
|
||||
.annotate(**fenhong_agg) \
|
||||
.order_by('period')
|
||||
|
||||
# 合并数据
|
||||
data_map = {}
|
||||
for item in chongzhi_qs:
|
||||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||
item['period'].strftime('%Y')
|
||||
data_map[period_str] = {
|
||||
'chongzhi_count': item['chongzhi_count'],
|
||||
'chongzhi_amount': float(item['chongzhi_amount'] or 0)
|
||||
}
|
||||
|
||||
if fenhong_qs:
|
||||
for item in fenhong_qs:
|
||||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||
item['period'].strftime('%Y')
|
||||
if period_str not in data_map:
|
||||
data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0}
|
||||
if include_guanshi:
|
||||
data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0)
|
||||
if include_zuzhang:
|
||||
data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0)
|
||||
|
||||
time_series = []
|
||||
for period_str, vals in data_map.items():
|
||||
guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0
|
||||
zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0
|
||||
vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2)
|
||||
vals['date'] = period_str
|
||||
time_series.append(vals)
|
||||
time_series.sort(key=lambda x: x['date'])
|
||||
|
||||
# ---------- 汇总数据 ----------
|
||||
summary_chongzhi_filter = chongzhi_filter.copy()
|
||||
if granularity == 'day':
|
||||
summary_chongzhi_filter['CreateTime__date'] = summary_q
|
||||
elif granularity == 'month':
|
||||
summary_chongzhi_filter['CreateTime__year'] = summary_q.year
|
||||
summary_chongzhi_filter['CreateTime__month'] = summary_q.month
|
||||
else:
|
||||
summary_chongzhi_filter['CreateTime__year'] = summary_q.year
|
||||
|
||||
summary_chongzhi = Czjilu.query.filter(**summary_chongzhi_filter).aggregate(
|
||||
total_count=Count('id'),
|
||||
total_amount=Sum('jine')
|
||||
)
|
||||
total_chongzhi_count = summary_chongzhi['total_count'] or 0
|
||||
total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0)
|
||||
|
||||
total_guanshi = 0.0
|
||||
total_zuzhang = 0.0
|
||||
if include_guanshi or include_zuzhang:
|
||||
summary_fenhong_filter = fenhong_filter.copy()
|
||||
if granularity == 'day':
|
||||
summary_fenhong_filter['CreateTime__date'] = summary_q
|
||||
elif granularity == 'month':
|
||||
summary_fenhong_filter['CreateTime__year'] = summary_q.year
|
||||
summary_fenhong_filter['CreateTime__month'] = summary_q.month
|
||||
else:
|
||||
summary_fenhong_filter['CreateTime__year'] = summary_q.year
|
||||
|
||||
fenhong_agg = {}
|
||||
if include_guanshi:
|
||||
fenhong_agg['guanshi_sum'] = Sum('fenhong')
|
||||
if include_zuzhang:
|
||||
fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong')
|
||||
summary_fenhong = Gsfenhong.query.filter(**summary_fenhong_filter).aggregate(**fenhong_agg)
|
||||
total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0)
|
||||
total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0)
|
||||
|
||||
total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'time_series': time_series,
|
||||
'summary': {
|
||||
'total_chongzhi_count': total_chongzhi_count,
|
||||
'total_chongzhi_amount': total_chongzhi_amount,
|
||||
'total_guanshi_fenhong': total_guanshi,
|
||||
'total_zuzhang_fenhong': total_zuzhang,
|
||||
'total_shouyi': total_shouyi
|
||||
}
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("HybkjtsjView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SzxxView(APIView):
|
||||
"""
|
||||
每日收支详细统计数据
|
||||
POST /houtai/szxx
|
||||
只读 daily_income_stat / daily_payout_stat,按 date 字段汇总。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
from jituan.services.szxx_stats import build_szxx_payload
|
||||
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
|
||||
if 'caiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||||
|
||||
granularity = request.data.get('granularity', 'day')
|
||||
year = request.data.get('year')
|
||||
month = request.data.get('month')
|
||||
summary_date = request.data.get('summary_date')
|
||||
|
||||
data, err = build_szxx_payload(request, granularity, year, month, summary_date)
|
||||
if err:
|
||||
return Response({'code': 1, 'msg': err}, status=400)
|
||||
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("SzxxView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class CwddhqlxView(APIView):
|
||||
"""
|
||||
获取订单类型(商品类型)用于筛选
|
||||
POST /houtai/cwddhqlx
|
||||
Body: { "phone": "管理员手机号" }
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
|
||||
if 'dingdancaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||||
|
||||
# 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称
|
||||
types = ShangpinLeixing.query.filter(shenhezhuangtai=1).values('id', 'jieshao')
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': list(types)
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("CwddhqlxView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
class CwhqjtddsjView(APIView):
|
||||
"""
|
||||
订单详细统计数据(修正版)
|
||||
POST /houtai/cwhqjtddsj
|
||||
Body: {
|
||||
"phone": "管理员手机号",
|
||||
"granularity": "day" | "month" | "year",
|
||||
"year": 2026,
|
||||
"month": 5,
|
||||
"type_id": 1, # 商品类型ID,0表示全部
|
||||
"order_source": 1, # 1=平台单,2=商家单,0=全部
|
||||
"summary_date": "2026-05-16"
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'dingdancaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||||
|
||||
granularity = request.data.get('granularity', 'day')
|
||||
year = request.data.get('year')
|
||||
month = request.data.get('month')
|
||||
type_id = request.data.get('type_id', 0)
|
||||
order_source = request.data.get('order_source', 0)
|
||||
summary_date = request.data.get('summary_date')
|
||||
|
||||
now = date.today()
|
||||
|
||||
# ---------- 时间范围 ----------
|
||||
if granularity == 'day':
|
||||
if not year or not month:
|
||||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||||
start_date = date(year, month, 1)
|
||||
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
|
||||
trunc_func = TruncDate('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = now.strftime('%Y-%m-%d')
|
||||
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||||
elif granularity == 'month':
|
||||
if not year:
|
||||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year + 1, 1, 1)
|
||||
trunc_func = TruncMonth('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = now.strftime('%Y-%m')
|
||||
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||||
elif granularity == 'year':
|
||||
year = year or now.year
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year + 1, 1, 1)
|
||||
trunc_func = TruncYear('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = str(year)
|
||||
summary_q = date(year, 1, 1)
|
||||
else:
|
||||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||||
|
||||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||||
|
||||
# ---------- 基础过滤 ----------
|
||||
filters = {
|
||||
'CreateTime__gte': start_dt,
|
||||
'CreateTime__lt': end_dt,
|
||||
'Status__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态
|
||||
}
|
||||
if int(type_id) != 0:
|
||||
filters['ProductTypeID'] = type_id
|
||||
if int(order_source) in [1, 2]:
|
||||
filters['Platform'] = order_source
|
||||
|
||||
# ---------- 时间序列分组查询 ----------
|
||||
order_base = filter_queryset_by_club(
|
||||
Order.query.filter(**filters), request, club_field='ClubID',
|
||||
)
|
||||
qs = order_base \
|
||||
.annotate(period=trunc_func) \
|
||||
.values('period') \
|
||||
.annotate(
|
||||
# 订单总量(所有符合条件的状态)
|
||||
order_count=Count('id'),
|
||||
# 订单总额
|
||||
order_amount=Sum('Amount'),
|
||||
# 成交单量 (Status=3)
|
||||
success_count=Count('id', filter=Q(Status=3)),
|
||||
# 成交订单总额
|
||||
success_amount=Sum('Amount', filter=Q(Status=3)),
|
||||
# 退款单量 (Status=5)
|
||||
refund_count=Count('id', filter=Q(Status=5)),
|
||||
# 打手分成总额(仅成交订单)
|
||||
PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)),
|
||||
# 平台成交订单的店铺分红
|
||||
dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome',
|
||||
filter=Q(Status=3, Platform=1)),
|
||||
# 成交订单中:平台单金额、商家单金额、各自打手分成
|
||||
platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
|
||||
merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
|
||||
platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
|
||||
merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
|
||||
).order_by('period')
|
||||
|
||||
time_series = []
|
||||
for item in qs:
|
||||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||
item['period'].strftime('%Y')
|
||||
|
||||
# 平台收益计算
|
||||
platform_profit = (item['platform_success_amount'] or 0) \
|
||||
- (item['platform_dashou'] or 0) \
|
||||
- (item['dianpu_fenhong_sum'] or 0)
|
||||
merchant_profit = (item['merchant_success_amount'] or 0) \
|
||||
- (item['merchant_dashou'] or 0)
|
||||
total_profit = platform_profit + merchant_profit
|
||||
|
||||
time_series.append({
|
||||
'date': period_str,
|
||||
'order_count': item['order_count'],
|
||||
'order_amount': float(item['order_amount'] or 0),
|
||||
'success_count': item['success_count'],
|
||||
'success_amount': float(item['success_amount'] or 0),
|
||||
'refund_count': item['refund_count'],
|
||||
'dashou_fencheng': float(item['PlayerCommission_sum'] or 0),
|
||||
'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
|
||||
'profit': round(total_profit, 2)
|
||||
})
|
||||
|
||||
# ---------- 汇总数据 ----------
|
||||
summary_qs = filter_queryset_by_club(
|
||||
Order.query.filter(**filters), request, club_field='ClubID',
|
||||
)
|
||||
agg = summary_qs.aggregate(
|
||||
total_order_count=Count('id'),
|
||||
total_order_amount=Sum('Amount'),
|
||||
total_success_count=Count('id', filter=Q(Status=3)),
|
||||
total_success_amount=Sum('Amount', filter=Q(Status=3)),
|
||||
total_refund_count=Count('id', filter=Q(Status=5)),
|
||||
total_dashou=Sum('PlayerCommission', filter=Q(Status=3)),
|
||||
total_dianpu_fenhong=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
|
||||
platform_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
|
||||
merchant_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
|
||||
platform_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
|
||||
merchant_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
|
||||
)
|
||||
total_order_count = agg['total_order_count'] or 0
|
||||
total_order_amount = agg['total_order_amount'] or 0
|
||||
total_success_count = agg['total_success_count'] or 0
|
||||
total_success_amount = agg['total_success_amount'] or 0
|
||||
total_refund_count = agg['total_refund_count'] or 0
|
||||
total_dashou = agg['total_dashou'] or 0
|
||||
total_dianpu_fenhong = agg['total_dianpu_fenhong'] or 0
|
||||
platform_amount = agg['platform_amount'] or 0
|
||||
merchant_amount = agg['merchant_amount'] or 0
|
||||
platform_dashou_sum = agg['platform_dashou_sum'] or 0
|
||||
merchant_dashou_sum = agg['merchant_dashou_sum'] or 0
|
||||
|
||||
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
|
||||
profit_merchant = merchant_amount - merchant_dashou_sum
|
||||
total_profit = profit_platform + profit_merchant
|
||||
|
||||
summary = {
|
||||
'total_order_count': total_order_count,
|
||||
'total_order_amount': round(float(total_order_amount), 2),
|
||||
'total_success_count': total_success_count,
|
||||
'total_success_amount': round(float(total_success_amount), 2),
|
||||
'total_refund_count': total_refund_count,
|
||||
'total_PlayerCommission': round(float(total_dashou), 2),
|
||||
'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2),
|
||||
'total_profit': round(total_profit, 2)
|
||||
}
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'time_series': time_series,
|
||||
'summary': summary
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("CwhqjtddsjView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class CwqtczhqView(APIView):
|
||||
"""
|
||||
其他充值/罚款数据统计(非会员充值)
|
||||
POST /houtai/cwqtczhq
|
||||
Body: {
|
||||
"phone": "管理员手机号",
|
||||
"granularity": "day" | "month" | "year",
|
||||
"year": 2026,
|
||||
"month": 5, // day时必填
|
||||
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
|
||||
"summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'qtczcaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
|
||||
|
||||
from jituan.services.finance_detail_stats import build_chongzhi_finance_payload
|
||||
|
||||
granularity = request.data.get('granularity', 'day')
|
||||
year = request.data.get('year')
|
||||
month = request.data.get('month')
|
||||
types = request.data.get('types', [2, 3, 4, 5, 6])
|
||||
summary_date = request.data.get('summary_date')
|
||||
|
||||
if isinstance(types, list) and len(types) == 0:
|
||||
types = [2, 3, 4, 5, 6]
|
||||
|
||||
data, err = build_chongzhi_finance_payload(
|
||||
request, granularity, year, month, types, summary_date,
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 1, 'msg': err}, status=400)
|
||||
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception as e:
|
||||
logger.exception("CwqtczhqView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
'''class CwqtczhqView_LEGACY(APIView):
|
||||
"""
|
||||
其他充值/罚款数据统计(非会员充值)
|
||||
POST /houtai/cwqtczhq
|
||||
Body: {
|
||||
"phone": "管理员手机号",
|
||||
"granularity": "day" | "month" | "year",
|
||||
"year": 2026,
|
||||
"month": 5, // day时必填
|
||||
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
|
||||
"summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'qtczcaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
|
||||
|
||||
granularity = request.data.get('granularity', 'day')
|
||||
year = request.data.get('year')
|
||||
month = request.data.get('month')
|
||||
types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部
|
||||
summary_date = request.data.get('summary_date')
|
||||
|
||||
if isinstance(types, list) and len(types) == 0:
|
||||
types = [2, 3, 4, 5, 6]
|
||||
|
||||
now = date.today()
|
||||
|
||||
# ---------- 时间范围(用于曲线图) ----------
|
||||
if granularity == 'day':
|
||||
if not year or not month:
|
||||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||||
start_date = date(year, month, 1)
|
||||
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
|
||||
trunc_func = TruncDate('CreateTime')
|
||||
# 默认汇总日期
|
||||
if not summary_date:
|
||||
summary_date = now.strftime('%Y-%m-%d')
|
||||
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||||
elif granularity == 'month':
|
||||
if not year:
|
||||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year + 1, 1, 1)
|
||||
trunc_func = TruncMonth('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = now.strftime('%Y-%m')
|
||||
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||||
elif granularity == 'year':
|
||||
year = year or now.year
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year + 1, 1, 1)
|
||||
trunc_func = TruncYear('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = str(year)
|
||||
summary_q = date(year, 1, 1)
|
||||
else:
|
||||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||||
|
||||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||||
|
||||
# ---------- 构建时间序列 ----------
|
||||
time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
|
||||
|
||||
def ensure_date(ds):
|
||||
if ds not in time_series_map:
|
||||
time_series_map[ds] = {}
|
||||
return time_series_map[ds]
|
||||
|
||||
# 1. 处理充值记录(Czjilu)
|
||||
cz_types = [t for t in types if t in [2, 3, 4, 5]]
|
||||
if cz_types:
|
||||
cz_qs = Czjilu.query.filter(
|
||||
CreateTime__gte=start_dt,
|
||||
CreateTime__lt=end_dt,
|
||||
leixing__in=cz_types,
|
||||
zhuangtai=3 # 只统计支付成功的
|
||||
).annotate(
|
||||
period=trunc_func
|
||||
).values('period', 'leixing').annotate(
|
||||
total_count=Count('id'),
|
||||
total_amount=Sum('jine')
|
||||
).order_by('period', 'leixing')
|
||||
|
||||
for item in cz_qs:
|
||||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||
item['period'].strftime('%Y')
|
||||
lx = item['leixing']
|
||||
amount = float(item['total_amount'] or 0)
|
||||
count = item['total_count']
|
||||
# 收益:积分(3)收益为金额本身,其他为0
|
||||
profit = amount if lx == 3 else 0.0
|
||||
|
||||
day_data = ensure_date(period_str)
|
||||
day_data[str(lx)] = {
|
||||
'count': count,
|
||||
'amount': amount,
|
||||
'profit': profit,
|
||||
'fenhong_count': 0, # 充值类型无分红
|
||||
'fenhong_amount': 0.0
|
||||
}
|
||||
|
||||
# 2. 处理罚款(Penalty)—— 包含分红
|
||||
if 6 in types:
|
||||
# 罚款记录
|
||||
penalty_qs = Penalty.query.filter(
|
||||
CreateTime__gte=start_dt,
|
||||
CreateTime__lt=end_dt,
|
||||
Status=2 # 已缴纳
|
||||
).annotate(
|
||||
period=trunc_func
|
||||
).values('period').annotate(
|
||||
penalty_count=Count('id'),
|
||||
penalty_amount=Sum('FineAmount')
|
||||
).order_by('period')
|
||||
|
||||
# 分红记录
|
||||
fenhong_qs = PenaltyBonus.query.filter(
|
||||
CreateTime__gte=start_dt,
|
||||
CreateTime__lt=end_dt
|
||||
).annotate(
|
||||
period=trunc_func
|
||||
).values('period').annotate(
|
||||
fh_count=Count('id'),
|
||||
fh_amount=Sum('WithdrawableAmount')
|
||||
).order_by('period')
|
||||
|
||||
# 合并
|
||||
for item in penalty_qs:
|
||||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||
item['period'].strftime('%Y')
|
||||
day_data = ensure_date(period_str)
|
||||
day_data['6'] = {
|
||||
'count': item['penalty_count'],
|
||||
'amount': float(item['penalty_amount'] or 0),
|
||||
'profit': 0.0, # 先设为0,后续计算
|
||||
'fenhong_count': 0,
|
||||
'fenhong_amount': 0.0
|
||||
}
|
||||
|
||||
for item in fenhong_qs:
|
||||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||
item['period'].strftime('%Y')
|
||||
day_data = ensure_date(period_str)
|
||||
if '6' not in day_data:
|
||||
day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0}
|
||||
day_data['6']['fenhong_count'] += item['fh_count']
|
||||
day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0)
|
||||
|
||||
# 计算罚款收益 = 罚款金额 - 分红金额
|
||||
for period_str, day_data in time_series_map.items():
|
||||
if '6' in day_data:
|
||||
penalty = day_data['6']
|
||||
penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2)
|
||||
|
||||
# 构造有序时间序列列表
|
||||
time_series = []
|
||||
for period_str in sorted(time_series_map.keys()):
|
||||
entry = {'date': period_str, 'types': {}}
|
||||
for type_code in sorted(time_series_map[period_str].keys()):
|
||||
entry['types'][type_code] = time_series_map[period_str][type_code]
|
||||
time_series.append(entry)
|
||||
|
||||
# ---------- 汇总指定日期 ----------
|
||||
summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
|
||||
|
||||
# 汇总充值类型
|
||||
for lx in cz_types:
|
||||
filter_kwargs = {
|
||||
'leixing': lx,
|
||||
'zhuangtai': 3,
|
||||
}
|
||||
if granularity == 'day':
|
||||
filter_kwargs['CreateTime__date'] = summary_q
|
||||
elif granularity == 'month':
|
||||
filter_kwargs['CreateTime__year'] = summary_q.year
|
||||
filter_kwargs['CreateTime__month'] = summary_q.month
|
||||
else:
|
||||
filter_kwargs['CreateTime__year'] = summary_q.year
|
||||
agg = filter_queryset_by_club(
|
||||
Czjilu.query.filter(**filter_kwargs), request,
|
||||
).aggregate(count=Count('id'), amount=Sum('jine'))
|
||||
count = agg['count'] or 0
|
||||
amount = float(agg['amount'] or 0)
|
||||
profit = amount if lx == 3 else 0.0
|
||||
summary_result['by_type'][str(lx)] = {
|
||||
'count': count,
|
||||
'amount': round(amount, 2),
|
||||
'profit': round(profit, 2),
|
||||
'fenhong_count': 0,
|
||||
'fenhong_amount': 0.0
|
||||
}
|
||||
summary_result['total_count'] += count
|
||||
summary_result['total_amount'] += amount
|
||||
summary_result['total_profit'] += profit
|
||||
|
||||
# 汇总罚款
|
||||
if 6 in types:
|
||||
penalty_filter = {
|
||||
'Status': 2,
|
||||
}
|
||||
fenhong_filter = {}
|
||||
if granularity == 'day':
|
||||
penalty_filter['CreateTime__date'] = summary_q
|
||||
fenhong_filter['CreateTime__date'] = summary_q
|
||||
elif granularity == 'month':
|
||||
penalty_filter['CreateTime__year'] = summary_q.year
|
||||
penalty_filter['CreateTime__month'] = summary_q.month
|
||||
fenhong_filter['CreateTime__year'] = summary_q.year
|
||||
fenhong_filter['CreateTime__month'] = summary_q.month
|
||||
else:
|
||||
penalty_filter['CreateTime__year'] = summary_q.year
|
||||
fenhong_filter['CreateTime__year'] = summary_q.year
|
||||
|
||||
penalty_agg = Penalty.query.filter(**penalty_filter).aggregate(
|
||||
count=Count('id'), amount=Sum('FineAmount'))
|
||||
penalty_count = penalty_agg['count'] or 0
|
||||
penalty_amount = float(penalty_agg['amount'] or 0)
|
||||
|
||||
fenhong_agg = PenaltyBonus.query.filter(**fenhong_filter).aggregate(
|
||||
fh_count=Count('id'), fh_amount=Sum('WithdrawableAmount'))
|
||||
fh_count = fenhong_agg['fh_count'] or 0
|
||||
fh_amount = float(fenhong_agg['fh_amount'] or 0)
|
||||
|
||||
profit = round(penalty_amount - fh_amount, 2)
|
||||
summary_result['by_type']['6'] = {
|
||||
'count': penalty_count,
|
||||
'amount': round(penalty_amount, 2),
|
||||
'profit': profit,
|
||||
'fenhong_count': fh_count,
|
||||
'fenhong_amount': round(fh_amount, 2)
|
||||
}
|
||||
summary_result['total_count'] += penalty_count
|
||||
summary_result['total_amount'] += penalty_amount
|
||||
summary_result['total_profit'] += profit
|
||||
|
||||
summary_result['total_amount'] = round(summary_result['total_amount'], 2)
|
||||
summary_result['total_profit'] = round(summary_result['total_profit'], 2)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'time_series': time_series,
|
||||
'summary': summary_result
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("CwqtczhqView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)'''
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ==================== 后端接口 ====================
|
||||
# 文件:views.py (添加以下两个视图类)
|
||||
|
||||
|
||||
|
||||
860
backend/views/guanli.py
Normal file
860
backend/views/guanli.py
Normal file
@@ -0,0 +1,860 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
class GetGuanliListView(APIView):
|
||||
"""
|
||||
获取管事列表
|
||||
权限:4400a,4400b,4400c,4400d,4400e,4400f 任一
|
||||
POST /houtai/hthqgslb
|
||||
参数:
|
||||
username, keyword(可选), balance_op/balance_value, commission_op/commission_value,
|
||||
invite_count_op/invite_count_value, status, page, page_size
|
||||
返回:
|
||||
code:0, data: { list: [...], total }
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
|
||||
# 权限校验(需要任一权限)
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
|
||||
if not any(p in permissions for p in required_perms):
|
||||
return Response({'code': 403, 'msg': '您没有权限查看管事列表'})
|
||||
|
||||
# 分页参数
|
||||
try:
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 20))
|
||||
if page < 1 or page_size < 1:
|
||||
raise ValueError
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '分页参数错误'})
|
||||
|
||||
# 筛选参数
|
||||
keyword = request.data.get('keyword', '').strip()
|
||||
balance_op = request.data.get('balance_op', '').strip().lower()
|
||||
balance_value = request.data.get('balance_value')
|
||||
commission_op = request.data.get('commission_op', '').strip().lower()
|
||||
commission_value = request.data.get('commission_value')
|
||||
invite_count_op = request.data.get('invite_count_op', '').strip().lower()
|
||||
invite_count_value = request.data.get('invite_count_value')
|
||||
status = request.data.get('status')
|
||||
|
||||
# 构建查询:从 User 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称)
|
||||
qs = filter_user_qs_by_club(
|
||||
User.query.filter(
|
||||
GuanshiProfile__isnull=False # 确保是管事
|
||||
).select_related('GuanshiProfile').select_related('BossProfile'),
|
||||
request,
|
||||
)
|
||||
|
||||
# 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname)
|
||||
if keyword:
|
||||
qs = qs.filter(
|
||||
Q(UserUID__icontains=keyword) |
|
||||
Q(BossProfile__nickname__icontains=keyword)
|
||||
)
|
||||
|
||||
# 余额筛选(使用 guanshi_profile.yue)
|
||||
if balance_op and balance_value is not None and balance_value != '':
|
||||
try:
|
||||
balance_val = Decimal(str(balance_value))
|
||||
if balance_op == 'gte':
|
||||
qs = qs.filter(GuanshiProfile__yue__gte=balance_val)
|
||||
elif balance_op == 'lte':
|
||||
qs = qs.filter(GuanshiProfile__yue__lte=balance_val)
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '余额比较符无效'})
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '余额数值格式错误'})
|
||||
|
||||
# 分佣总额筛选(guanshi_profile.chongzhifenrun)
|
||||
if commission_op and commission_value is not None and commission_value != '':
|
||||
try:
|
||||
commission_val = Decimal(str(commission_value))
|
||||
if commission_op == 'gte':
|
||||
qs = qs.filter(GuanshiProfile__chongzhifenrun__gte=commission_val)
|
||||
elif commission_op == 'lte':
|
||||
qs = qs.filter(GuanshiProfile__chongzhifenrun__lte=commission_val)
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '分佣比较符无效'})
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '分佣数值格式错误'})
|
||||
|
||||
# 邀请打手总数筛选(guanshi_profile.yaogingshuliang)
|
||||
if invite_count_op and invite_count_value is not None and invite_count_value != '':
|
||||
try:
|
||||
invite_val = int(invite_count_value)
|
||||
if invite_count_op == 'gte':
|
||||
qs = qs.filter(GuanshiProfile__yaogingshuliang__gte=invite_val)
|
||||
elif invite_count_op == 'lte':
|
||||
qs = qs.filter(GuanshiProfile__yaogingshuliang__lte=invite_val)
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '邀请数量比较符无效'})
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '邀请数量格式错误'})
|
||||
|
||||
# 状态筛选
|
||||
if status is not None and status != '':
|
||||
try:
|
||||
status_val = int(status)
|
||||
if status_val not in (0, 1):
|
||||
raise ValueError
|
||||
qs = qs.filter(GuanshiProfile__zhuangtai=status_val)
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '状态值无效'})
|
||||
|
||||
# 计算总数
|
||||
total = qs.count()
|
||||
|
||||
# 分页
|
||||
offset = (page - 1) * page_size
|
||||
users = qs.order_by('-UserCreateTime')[offset:offset + page_size]
|
||||
|
||||
# 构建返回数据
|
||||
result = []
|
||||
for user in users:
|
||||
guanshi = user.GuanshiProfile
|
||||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||
result.append({
|
||||
'yonghuid': user.UserUID,
|
||||
'avatar': user.Avatar or '',
|
||||
'nickname': boss.nickname if boss else '',
|
||||
'zhuangtai': guanshi.zhuangtai,
|
||||
'yaogingshuliang': guanshi.yaogingshuliang,
|
||||
'yue': str(guanshi.yue),
|
||||
'chongzhifenrun': str(guanshi.chongzhifenrun),
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
**list_response_meta(request),
|
||||
'data': {'list': result, 'total': total},
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class GetGuanliDetailView(APIView):
|
||||
"""
|
||||
获取管事详情
|
||||
权限:4400a,4400b,4400c,4400d,4400e,4400f 任一
|
||||
POST /houtai/hthqgsxq
|
||||
参数:username, yonghuid
|
||||
返回:
|
||||
code:0, data: {
|
||||
base: {...},
|
||||
balance: {...},
|
||||
permanent_enabled, permanent_amount,
|
||||
custom_first_dividend: {},
|
||||
extra_dividend_map: {},
|
||||
all_members: []
|
||||
}
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
yonghuid = request.data.get('yonghuid', '').strip()
|
||||
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
if not yonghuid:
|
||||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||
|
||||
# 权限校验(需要任一权限)
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
|
||||
if not any(p in permissions for p in required_perms):
|
||||
return Response({'code': 403, 'msg': '您没有权限查看管事详情'})
|
||||
|
||||
# 查询用户主表和管事扩展表
|
||||
try:
|
||||
user = User.query.select_related('GuanshiProfile', 'BossProfile').get(UserUID=yonghuid)
|
||||
guanshi = user.GuanshiProfile
|
||||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||
except User.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '管事不存在'})
|
||||
except UserGuanshi.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不是管事'})
|
||||
|
||||
denied = forbid_if_user_out_of_scope(request, user)
|
||||
if denied:
|
||||
return denied
|
||||
|
||||
# 基础信息
|
||||
is_zuzhang = UserZuzhang.query.filter(user=user).exists()
|
||||
base_data = {
|
||||
'yonghuid': user.UserUID,
|
||||
'nickname': boss.nickname if boss else '',
|
||||
'avatar': user.Avatar or '',
|
||||
'dianhua': guanshi.dianhua or '',
|
||||
'wechat': guanshi.wechat or '',
|
||||
'zhuangtai': guanshi.zhuangtai,
|
||||
'yaoqingma': guanshi.yaoqingma,
|
||||
'yaoqingren': guanshi.yaoqingren,
|
||||
'invite_qrcode_url': guanshi.invite_qrcode_url or '',
|
||||
'yaogingshuliang': guanshi.yaogingshuliang,
|
||||
'jinrichongzhi': guanshi.jinrichongzhi,
|
||||
'jinyuechongzhi': guanshi.jinyuechongzhi,
|
||||
'chongzhifenrun': str(guanshi.chongzhifenrun),
|
||||
'create_time': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None,
|
||||
'CreateTime': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None,
|
||||
}
|
||||
|
||||
# 余额提现相关
|
||||
balance_data = {
|
||||
'yue': str(guanshi.yue),
|
||||
'jinritixian_jine': str(guanshi.jinritixian_jine),
|
||||
'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None,
|
||||
'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane,
|
||||
'ewai_xiane': str(guanshi.ewai_xiane),
|
||||
}
|
||||
|
||||
# 永久分红(二次分红)
|
||||
permanent_enabled = guanshi.fenghong_erci_enabled
|
||||
permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0'
|
||||
|
||||
# 获取所有会员(用于前端选择)
|
||||
all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc'))
|
||||
for m in all_members:
|
||||
m['jiage'] = str(m['jiage'])
|
||||
m['guanshifc'] = str(m['guanshifc'])
|
||||
|
||||
# 获取多次分红配置(按次数和会员组织,按俱乐部隔离)
|
||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||
duoci_records = DuociFenhong.query.filter(
|
||||
yonghuid=yonghuid, club_id=guanshi_club,
|
||||
).order_by('cishu')
|
||||
custom_first = {} # 首次分红定制(cishu=1)
|
||||
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } }
|
||||
|
||||
# 批量预取会员信息
|
||||
_huiyuan_ids = [r.huiyuan for r in duoci_records]
|
||||
_huiyuan_map = {
|
||||
h.huiyuan_id: h
|
||||
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao', 'jiage')
|
||||
}
|
||||
|
||||
for record in duoci_records:
|
||||
# 获取会员信息
|
||||
member = _huiyuan_map.get(record.huiyuan)
|
||||
if not member:
|
||||
continue
|
||||
member_info = {
|
||||
'jieshao': member.jieshao,
|
||||
'jiage': str(member.jiage),
|
||||
}
|
||||
if record.cishu == 1:
|
||||
custom_first[record.huiyuan] = float(record.guanshi_fenhong)
|
||||
else:
|
||||
times = record.cishu
|
||||
if times not in extra_map:
|
||||
extra_map[times] = {}
|
||||
extra_map[times][record.huiyuan] = {
|
||||
'guanshi_fenhong': float(record.guanshi_fenhong),
|
||||
**member_info
|
||||
}
|
||||
|
||||
# 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示)
|
||||
# 额外次数分红按次数返回
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'base': base_data,
|
||||
'balance': balance_data,
|
||||
'permanent_enabled': permanent_enabled,
|
||||
'permanent_amount': permanent_amount,
|
||||
'custom_first_dividend': custom_first,
|
||||
'extra_dividend_map': extra_map,
|
||||
'all_members': all_members,
|
||||
'is_zuzhang': is_zuzhang,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
def _generate_unique_zuzhang_yaoqingma(yonghuid):
|
||||
"""生成唯一组长邀请码(与小程序组长注册逻辑一致)"""
|
||||
timestamp = str(int(time.time()))[-6:]
|
||||
uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
|
||||
rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
|
||||
base = f"Z{timestamp}{uid_part}{rand_str}"
|
||||
for _ in range(5):
|
||||
if not UserZuzhang.query.filter(yaoqingma=base).exists():
|
||||
return base
|
||||
base = base + random.choice(string.digits)
|
||||
return base + str(int(time.time() * 1000))[-4:]
|
||||
|
||||
|
||||
def _create_zuzhang_profile(user):
|
||||
"""为已有用户创建组长扩展记录(默认字段与小程序注册一致)"""
|
||||
invite_guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
|
||||
yaoqingma = _generate_unique_zuzhang_yaoqingma(user.UserUID)
|
||||
return UserZuzhang.query.create(
|
||||
user=user,
|
||||
yaoqingma=yaoqingma,
|
||||
fenyong_zonge=Decimal('0.00'),
|
||||
ketixian_jine=Decimal('0.00'),
|
||||
yaoqing_zongshu=invite_guanshi_count,
|
||||
jinri_fenyong=Decimal('0.00'),
|
||||
jinyue_fenyong=Decimal('0.00'),
|
||||
zhuangtai=1,
|
||||
jinri_tixian=Decimal('0.00'),
|
||||
ewai_tixian_xiane=Decimal('0.00'),
|
||||
kaioi_ewai_tixian=False,
|
||||
kaioi_ewai_fenhong=False,
|
||||
ewai_fenhong_jine=Decimal('0.00'),
|
||||
)
|
||||
|
||||
|
||||
class PromoteGuanshiToZuzhangView(APIView):
|
||||
"""
|
||||
后台将管事用户升级为组长(写入 user_zuzhang 扩展表)
|
||||
POST /houtai/htgssjzz
|
||||
权限:sjzz666
|
||||
参数:username, yonghuid
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
yonghuid = request.data.get('yonghuid', '').strip()
|
||||
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
if not yonghuid:
|
||||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if 'sjzz666' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限升级组长(需要 sjzz666)'})
|
||||
|
||||
try:
|
||||
user = User.query.select_related('GuanshiProfile').get(UserUID=yonghuid)
|
||||
guanshi = user.GuanshiProfile
|
||||
except User.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不存在'})
|
||||
except UserGuanshi.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不是管事,无法升级组长'})
|
||||
|
||||
if UserZuzhang.query.filter(user=user).exists():
|
||||
zuzhang = user.ZuzhangProfile
|
||||
return Response({
|
||||
'code': 400,
|
||||
'msg': '该用户已是组长',
|
||||
'data': {
|
||||
'yonghuid': user.UserUID,
|
||||
'yaoqingma': zuzhang.yaoqingma,
|
||||
},
|
||||
})
|
||||
|
||||
if guanshi.zhuangtai != 1:
|
||||
return Response({'code': 400, 'msg': '管事账号已禁用,无法升级组长'})
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
zuzhang = _create_zuzhang_profile(user)
|
||||
except IntegrityError:
|
||||
return Response({'code': 400, 'msg': '升级失败,请稍后重试'})
|
||||
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=kefu.user.Phone,
|
||||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||
qitashuoming=(
|
||||
f'【后台】管事升级组长:用户ID={yonghuid},'
|
||||
f'生成组长邀请码={zuzhang.yaoqingma},操作人={kefu.user.Phone}'
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(f"管事升级组长成功: yonghuid={yonghuid}, yaoqingma={zuzhang.yaoqingma}")
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '已升级为组长',
|
||||
'data': {
|
||||
'yonghuid': user.UserUID,
|
||||
'nickname': getattr(user.BossProfile, 'nickname', '') if hasattr(user, 'BossProfile') else '',
|
||||
'yaoqingma': zuzhang.yaoqingma,
|
||||
'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
|
||||
'ketixian_jine': str(zuzhang.ketixian_jine),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
class UpdateGuanliView(APIView):
|
||||
"""
|
||||
修改管事信息(基础信息、余额、提现限额、分红策略)
|
||||
权限细分:
|
||||
- 修改状态(封禁/解封)及昵称/电话/微信:需要4400a
|
||||
- 增加余额:需要4400c
|
||||
- 减少余额:需要4400b
|
||||
- 修改提现限额(kaioi_ewai_xiane/ewai_xiane):需要4400e
|
||||
- 修改分红相关(永久分红、定制首次、额外次数):需要4400f
|
||||
POST /houtai/htxggsxx
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
yonghuid = request.data.get('yonghuid', '').strip()
|
||||
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
if not yonghuid:
|
||||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||
|
||||
# 权限校验(先获取权限列表)
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
# 查询管事对象(不在事务外加锁,事务内加锁)
|
||||
try:
|
||||
user = User.query.get(UserUID=yonghuid)
|
||||
guanshi = user.GuanshiProfile
|
||||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||
except User.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不存在'})
|
||||
except UserGuanshi.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不是管事'})
|
||||
|
||||
# 开始事务,内部使用 select_for_update
|
||||
with transaction.atomic():
|
||||
# 重新获取加锁对象
|
||||
user = User.objects.select_for_update().get(UserUID=yonghuid)
|
||||
guanshi = user.GuanshiProfile
|
||||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||
operator = kefu.user.Phone
|
||||
|
||||
# ---------- 1. 基础信息修改(需要4400a)----------
|
||||
nickname = request.data.get('nickname')
|
||||
dianhua = request.data.get('dianhua')
|
||||
wechat = request.data.get('wechat')
|
||||
zhuangtai = request.data.get('zhuangtai')
|
||||
avatar = request.data.get('avatar')
|
||||
|
||||
if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]):
|
||||
if '4400a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'})
|
||||
base_changes = []
|
||||
old_nickname = boss.nickname if boss else ''
|
||||
old_dianhua = guanshi.dianhua
|
||||
old_wechat = guanshi.wechat
|
||||
old_zhuangtai = guanshi.zhuangtai
|
||||
# 处理昵称(在老板扩展表中)
|
||||
if nickname is not None:
|
||||
if not boss:
|
||||
boss = UserBoss.query.create(user=user, nickname=nickname)
|
||||
else:
|
||||
boss.nickname = nickname
|
||||
boss.save()
|
||||
if str(nickname) != str(old_nickname):
|
||||
base_changes.append(f'昵称:{old_nickname or "无"}→{nickname}')
|
||||
# 处理其他字段
|
||||
if dianhua is not None:
|
||||
guanshi.dianhua = dianhua
|
||||
if str(dianhua) != str(old_dianhua):
|
||||
base_changes.append(f'电话:{old_dianhua or "无"}→{dianhua}')
|
||||
if wechat is not None:
|
||||
guanshi.wechat = wechat
|
||||
if str(wechat) != str(old_wechat):
|
||||
base_changes.append(f'微信:{old_wechat or "无"}→{wechat}')
|
||||
if zhuangtai is not None:
|
||||
try:
|
||||
zt = int(zhuangtai)
|
||||
if zt in (0, 1):
|
||||
guanshi.zhuangtai = zt
|
||||
if zt != old_zhuangtai:
|
||||
zt_map = {0: '禁用', 1: '正常'}
|
||||
base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}→{zt_map.get(zt, zt)}')
|
||||
except:
|
||||
pass
|
||||
if avatar is not None:
|
||||
user.Avatar = avatar
|
||||
user.save()
|
||||
base_changes.append('更新了头像')
|
||||
guanshi.save()
|
||||
if base_changes:
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=operator,
|
||||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||
qitashuoming=(
|
||||
f'【后台】修改管事基础信息:{";".join(base_changes)},'
|
||||
f'管事ID={yonghuid},操作人={operator}'
|
||||
),
|
||||
)
|
||||
|
||||
# ---------- 2. 余额修改(加:4400c;减:4400b)----------
|
||||
new_yue = request.data.get('yue')
|
||||
if new_yue is not None:
|
||||
try:
|
||||
new_yue = Decimal(str(new_yue))
|
||||
if new_yue < 0:
|
||||
return Response({'code': 400, 'msg': '余额不能为负数'})
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '余额格式错误'})
|
||||
|
||||
old_yue = guanshi.yue
|
||||
if new_yue > old_yue:
|
||||
if '4400c' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'})
|
||||
from jituan.services.group_finance_gate import deny_group_finance_exec
|
||||
deny = deny_group_finance_exec(request.user, request)
|
||||
if deny:
|
||||
return deny
|
||||
action_desc = f'增加{new_yue - old_yue}元'
|
||||
elif new_yue < old_yue:
|
||||
if '4400b' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'})
|
||||
action_desc = f'减少{old_yue - new_yue}元'
|
||||
else:
|
||||
action_desc = '无变动'
|
||||
if new_yue != old_yue:
|
||||
guanshi.yue = new_yue
|
||||
guanshi.save()
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=operator,
|
||||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||
guanshiyue=new_yue,
|
||||
guanshiyueq=old_yue,
|
||||
qitashuoming=(
|
||||
f'【后台】修改管事可提现余额:{old_yue}元 → {new_yue}元({action_desc}),'
|
||||
f'管事ID={yonghuid},操作人={operator}'
|
||||
),
|
||||
)
|
||||
|
||||
# ---------- 3. 提现限额修改(需要4400e)----------
|
||||
kaioi_ewai = request.data.get('kaioi_ewai_xiane')
|
||||
ewai_val = request.data.get('ewai_xiane')
|
||||
if kaioi_ewai is not None or ewai_val is not None:
|
||||
if '4400e' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'})
|
||||
limit_changes = []
|
||||
old_kaioi = guanshi.kaioi_ewai_xiane
|
||||
old_ewai = guanshi.ewai_xiane
|
||||
if kaioi_ewai is not None:
|
||||
guanshi.kaioi_ewai_xiane = bool(kaioi_ewai)
|
||||
if bool(kaioi_ewai) != old_kaioi:
|
||||
limit_changes.append(f'额外限额开关:{"开" if old_kaioi else "关"}→{"开" if kaioi_ewai else "关"}')
|
||||
if ewai_val is not None:
|
||||
try:
|
||||
guanshi.ewai_xiane = Decimal(str(ewai_val))
|
||||
if guanshi.ewai_xiane != old_ewai:
|
||||
limit_changes.append(f'额外限额金额:{old_ewai}→{guanshi.ewai_xiane}')
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
|
||||
guanshi.save()
|
||||
if limit_changes:
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=operator,
|
||||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||
qitashuoming=(
|
||||
f'【后台】修改管事提现限额:{";".join(limit_changes)},'
|
||||
f'管事ID={yonghuid},操作人={operator}'
|
||||
),
|
||||
)
|
||||
|
||||
# ---------- 4. 分红相关修改(需要4400f)----------
|
||||
perm_enabled = request.data.get('permanent_enabled')
|
||||
perm_amount = request.data.get('permanent_amount')
|
||||
custom_first = request.data.get('custom_first_dividend')
|
||||
extra_map = request.data.get('extra_dividend_map')
|
||||
|
||||
if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
|
||||
if '4400f' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'})
|
||||
|
||||
# 4.1 永久分红
|
||||
if perm_enabled is not None:
|
||||
guanshi.fenghong_erci_enabled = bool(perm_enabled)
|
||||
if perm_amount is not None:
|
||||
try:
|
||||
guanshi.fenghong_erci = Decimal(str(perm_amount))
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '永久分红金额格式错误'})
|
||||
guanshi.save()
|
||||
|
||||
# 4.2 首次定制分红(cishu=1)
|
||||
if custom_first is not None and isinstance(custom_first, dict):
|
||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||
# 批量预取当前所有首次定制记录(避免循环内 N+1)
|
||||
existing_first_list = list(
|
||||
DuociFenhong.query.filter(
|
||||
club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
|
||||
)
|
||||
)
|
||||
_existing_map = {r.huiyuan: r for r in existing_first_list}
|
||||
_to_create = []
|
||||
_to_update = []
|
||||
_to_delete_ids = []
|
||||
# 前端传来的定制字典 {会员ID: 金额}
|
||||
for huiyuan_id, amount in custom_first.items():
|
||||
try:
|
||||
amount = Decimal(str(amount))
|
||||
except:
|
||||
continue
|
||||
if amount <= 0:
|
||||
rec = _existing_map.get(huiyuan_id)
|
||||
if rec is not None:
|
||||
_to_delete_ids.append(rec.id)
|
||||
else:
|
||||
rec = _existing_map.get(huiyuan_id)
|
||||
if rec is not None:
|
||||
rec.guanshi_fenhong = amount
|
||||
rec.zuzhang_fenhong = Decimal('0')
|
||||
_to_update.append(rec)
|
||||
else:
|
||||
_to_create.append(DuociFenhong(
|
||||
club_id=guanshi_club,
|
||||
yonghuid=yonghuid,
|
||||
huiyuan=huiyuan_id,
|
||||
cishu=1,
|
||||
guanshi_fenhong=amount,
|
||||
zuzhang_fenhong=Decimal('0'),
|
||||
))
|
||||
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
|
||||
for rec in existing_first_list:
|
||||
if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids:
|
||||
_to_delete_ids.append(rec.id)
|
||||
if _to_create:
|
||||
DuociFenhong.objects.bulk_create(_to_create)
|
||||
if _to_update:
|
||||
DuociFenhong.objects.bulk_update(_to_update, ['guanshi_fenhong', 'zuzhang_fenhong'])
|
||||
if _to_delete_ids:
|
||||
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
|
||||
|
||||
# 4.3 额外次数分红(cishu >= 2)
|
||||
if extra_map is not None and isinstance(extra_map, dict):
|
||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||
# 收集需要保留的记录
|
||||
keep_set = set()
|
||||
# 解析前端数据,先汇总 keep_set 与目标金额
|
||||
_extra_targets = [] # [(cishu, huiyuan_id, amount)]
|
||||
for times_str, members in extra_map.items():
|
||||
try:
|
||||
cishu = int(times_str)
|
||||
if cishu < 2:
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
if not isinstance(members, dict):
|
||||
continue
|
||||
for huiyuan_id, data in members.items():
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
amount = data.get('guanshi_fenhong')
|
||||
if amount is None:
|
||||
continue
|
||||
try:
|
||||
amount = Decimal(str(amount))
|
||||
except:
|
||||
continue
|
||||
keep_set.add((cishu, huiyuan_id))
|
||||
_extra_targets.append((cishu, huiyuan_id, amount))
|
||||
# 批量预取已有额外次数记录
|
||||
existing_extra_list = list(
|
||||
DuociFenhong.query.filter(
|
||||
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
|
||||
)
|
||||
)
|
||||
_extra_map = {(r.cishu, r.huiyuan): r for r in existing_extra_list}
|
||||
_to_create = []
|
||||
_to_update = []
|
||||
_to_delete_ids = []
|
||||
for cishu, huiyuan_id, amount in _extra_targets:
|
||||
rec = _extra_map.get((cishu, huiyuan_id))
|
||||
if rec is not None:
|
||||
rec.guanshi_fenhong = amount
|
||||
rec.zuzhang_fenhong = Decimal('0')
|
||||
_to_update.append(rec)
|
||||
else:
|
||||
_to_create.append(DuociFenhong(
|
||||
club_id=guanshi_club,
|
||||
yonghuid=yonghuid,
|
||||
huiyuan=huiyuan_id,
|
||||
cishu=cishu,
|
||||
guanshi_fenhong=amount,
|
||||
zuzhang_fenhong=Decimal('0'),
|
||||
))
|
||||
# 删除不在 keep_set 中的额外次数记录
|
||||
for rec in existing_extra_list:
|
||||
if (rec.cishu, rec.huiyuan) not in keep_set:
|
||||
_to_delete_ids.append(rec.id)
|
||||
if _to_create:
|
||||
DuociFenhong.objects.bulk_create(_to_create)
|
||||
if _to_update:
|
||||
DuociFenhong.objects.bulk_update(_to_update, ['guanshi_fenhong', 'zuzhang_fenhong'])
|
||||
if _to_delete_ids:
|
||||
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
|
||||
|
||||
dividend_parts = []
|
||||
if perm_enabled is not None:
|
||||
dividend_parts.append(f'永久分红开关→{"开" if perm_enabled else "关"}')
|
||||
if perm_amount is not None:
|
||||
dividend_parts.append(f'永久分红金额→{perm_amount}')
|
||||
if custom_first is not None:
|
||||
dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)')
|
||||
if extra_map is not None:
|
||||
dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)')
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=operator,
|
||||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||
qitashuoming=(
|
||||
f'【后台】修改管事分红策略:{";".join(dividend_parts) or "配置已更新"},'
|
||||
f'管事ID={yonghuid},操作人={operator}'
|
||||
),
|
||||
)
|
||||
|
||||
return Response({'code': 0, 'msg': '修改成功'})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1155
backend/views/kefu.py
Normal file
1155
backend/views/kefu.py
Normal file
File diff suppressed because it is too large
Load Diff
457
backend/views/members.py
Normal file
457
backend/views/members.py
Normal file
@@ -0,0 +1,457 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
class GetMemberListView(APIView):
|
||||
"""
|
||||
获取会员列表
|
||||
权限:3300a
|
||||
POST /houtai/hthqhylb
|
||||
参数:username
|
||||
返回:code:0, data: { list: [...] }
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '3300a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限查看会员列表'})
|
||||
|
||||
from jituan.services.club_member_admin import build_member_list_payload
|
||||
|
||||
payload = build_member_list_payload(request)
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {'list': payload['list']},
|
||||
'club_id': payload.get('club_id'),
|
||||
'scope': payload.get('scope'),
|
||||
'price_note': payload.get('price_note'),
|
||||
})
|
||||
|
||||
|
||||
class UpdateMemberView(APIView):
|
||||
"""
|
||||
修改会员信息
|
||||
权限:3300a
|
||||
POST /houtai/htxghyxx
|
||||
参数:
|
||||
username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
|
||||
返回:code:0 成功
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
huiyuan_id = request.data.get('huiyuan_id', '').strip()
|
||||
jieshao = request.data.get('jieshao', '').strip()
|
||||
jiage = request.data.get('jiage')
|
||||
guanshifc = request.data.get('guanshifc')
|
||||
zuzhangfc = request.data.get('zuzhangfc')
|
||||
jtjieshao = request.data.get('jtjieshao', '').strip()
|
||||
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
if not huiyuan_id:
|
||||
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||||
if not jieshao:
|
||||
return Response({'code': 400, 'msg': '会员名称不能为空'})
|
||||
|
||||
try:
|
||||
jiage = Decimal(str(jiage))
|
||||
guanshifc = Decimal(str(guanshifc))
|
||||
zuzhangfc = Decimal(str(zuzhangfc))
|
||||
if jiage < 0 or guanshifc < 0 or zuzhangfc < 0:
|
||||
raise ValueError
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||||
|
||||
if guanshifc + zuzhangfc > jiage:
|
||||
return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
|
||||
|
||||
# 权限校验
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '3300a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限修改会员'})
|
||||
|
||||
from jituan.services.club_member_admin import update_member_for_club
|
||||
|
||||
extra = {
|
||||
'formal_days': request.data.get('formal_days'),
|
||||
'trial_enabled': request.data.get('trial_enabled'),
|
||||
'trial_price': request.data.get('trial_price'),
|
||||
'trial_days': request.data.get('trial_days'),
|
||||
'trial_guanshifc': request.data.get('trial_guanshifc'),
|
||||
'trial_zuzhangfc': request.data.get('trial_zuzhangfc'),
|
||||
'card_image': request.data.get('card_image'),
|
||||
'card_bg': request.data.get('card_bg'),
|
||||
'included_huiyuan_ids': request.data.get('included_huiyuan_ids'),
|
||||
}
|
||||
data, err = update_member_for_club(
|
||||
request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, extra=extra,
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 400, 'msg': err})
|
||||
return Response({'code': 0, 'msg': '修改成功'})
|
||||
|
||||
|
||||
|
||||
class AddMemberView(APIView):
|
||||
"""
|
||||
添加会员
|
||||
权限:3300a
|
||||
POST /houtai/httjhy
|
||||
参数:
|
||||
username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
|
||||
返回:code:0, data: { huiyuan_id }
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
jieshao = request.data.get('jieshao', '').strip()
|
||||
jiage = request.data.get('jiage')
|
||||
guanshifc = request.data.get('guanshifc')
|
||||
zuzhangfc = request.data.get('zuzhangfc')
|
||||
jtjieshao = request.data.get('jtjieshao', '').strip()
|
||||
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
if not jieshao:
|
||||
return Response({'code': 400, 'msg': '会员名称不能为空'})
|
||||
|
||||
try:
|
||||
jiage = Decimal(str(jiage))
|
||||
guanshifc = Decimal(str(guanshifc))
|
||||
zuzhangfc = Decimal(str(zuzhangfc))
|
||||
if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0:
|
||||
raise ValueError
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||||
|
||||
if guanshifc + zuzhangfc > jiage:
|
||||
return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
|
||||
|
||||
# 权限校验
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '3300a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限添加会员'})
|
||||
|
||||
from jituan.services.club_member_admin import add_global_member
|
||||
from jituan.services.club_context import resolve_club_scope
|
||||
from jituan.constants import DATA_SCOPE_ALL
|
||||
|
||||
if resolve_club_scope(request) != DATA_SCOPE_ALL and '000001' not in permissions:
|
||||
return Response({
|
||||
'code': 403,
|
||||
'msg': '仅集团汇总视图可新建全局会员类型;俱乐部请从会员 catalog 上架',
|
||||
})
|
||||
|
||||
data, err = add_global_member(
|
||||
jieshao, jiage, guanshifc, zuzhangfc, jtjieshao,
|
||||
is_bundle=bool(request.data.get('is_bundle')),
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 400, 'msg': err})
|
||||
return Response({'code': 0, 'msg': '添加成功', 'data': data})
|
||||
|
||||
|
||||
|
||||
|
||||
class ClubMemberCatalogView(APIView):
|
||||
"""俱乐部可上架的集团会员 catalog。POST /houtai/hthycatalog"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '3300a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限'})
|
||||
from jituan.services.club_member_admin import build_member_catalog_payload
|
||||
return Response({'code': 0, 'data': build_member_catalog_payload(request)})
|
||||
|
||||
|
||||
class ClubMemberEnableView(APIView):
|
||||
"""俱乐部上架会员。POST /houtai/hthysj"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
huiyuan_id = request.data.get('huiyuan_id', '').strip()
|
||||
if not username or not huiyuan_id:
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '3300a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限'})
|
||||
try:
|
||||
jiage = Decimal(str(request.data.get('jiage')))
|
||||
guanshifc = Decimal(str(request.data.get('guanshifc')))
|
||||
zuzhangfc = Decimal(str(request.data.get('zuzhangfc')))
|
||||
except Exception:
|
||||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||||
from jituan.services.club_member_admin import enable_member_for_club
|
||||
extra = request.data
|
||||
data, err = enable_member_for_club(
|
||||
request, huiyuan_id, jiage, guanshifc, zuzhangfc, extra=extra,
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 400, 'msg': err})
|
||||
return Response({'code': 0, 'msg': '上架成功', 'data': data})
|
||||
|
||||
|
||||
class ClubMemberDisableView(APIView):
|
||||
"""俱乐部下架会员。POST /houtai/hthyxj"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
huiyuan_id = request.data.get('huiyuan_id', '').strip()
|
||||
if not username or not huiyuan_id:
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '3300a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限'})
|
||||
from jituan.services.club_member_admin import disable_member_for_club
|
||||
data, err = disable_member_for_club(request, huiyuan_id)
|
||||
if err:
|
||||
return Response({'code': 400, 'msg': err})
|
||||
return Response({'code': 0, 'msg': '已下架', 'data': data})
|
||||
|
||||
|
||||
class MemberCardImageUploadView(APIView):
|
||||
"""POST /jituan/houtai/hy-card-upload 上传会员充值卡片图"""
|
||||
permission_classes = []
|
||||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = (request.data.get('username') or request.data.get('phone') or '').strip()
|
||||
huiyuan_id = (request.data.get('huiyuan_id') or '').strip()
|
||||
field = (request.data.get('field') or 'card_image').strip()
|
||||
file_obj = request.FILES.get('file')
|
||||
if not username or not huiyuan_id or not file_obj:
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '3300a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限'})
|
||||
from jituan.services.club_member_admin import upload_member_card_image
|
||||
data, err = upload_member_card_image(request, huiyuan_id, field, file_obj)
|
||||
if err:
|
||||
return Response({'code': 400, 'msg': err})
|
||||
return Response({'code': 0, 'msg': '上传成功', 'data': data})
|
||||
|
||||
|
||||
class MemberRechargeRecordsView(APIView):
|
||||
"""POST /jituan/houtai/hy-recharge-records — 会员充值记录明细 + 分红"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = (request.data.get('username') or request.data.get('phone') or '').strip()
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '3300a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限查看会员充值记录'})
|
||||
|
||||
from jituan.services.member_recharge_records import build_member_recharge_records_payload
|
||||
|
||||
filters = request.data.get('filters') or {}
|
||||
for key in (
|
||||
'dingdan_id', 'yonghuid', 'buyer_keyword', 'huiyuan_id', 'is_trial',
|
||||
'zhuangtai', 'date_from', 'date_to', 'jine_min', 'jine_max',
|
||||
'guanshi_id', 'zuzhang_id', 'keyword', 'is_bundle', 'has_fenhong', 'club_id',
|
||||
):
|
||||
if request.data.get(key) not in (None, ''):
|
||||
filters[key] = request.data.get(key)
|
||||
|
||||
payload = build_member_recharge_records_payload(
|
||||
request,
|
||||
filters=filters,
|
||||
page=request.data.get('page', 1),
|
||||
page_size=request.data.get('page_size', 20),
|
||||
)
|
||||
return Response({'code': 0, 'data': payload})
|
||||
|
||||
|
||||
class MemberBundleCreateView(APIView):
|
||||
"""POST /jituan/houtai/hy-bundle-create — 俱乐部一键创建总会员并上架"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = (request.data.get('username') or request.data.get('phone') or '').strip()
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '3300a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限'})
|
||||
|
||||
jieshao = (request.data.get('jieshao') or '').strip()
|
||||
if not jieshao:
|
||||
return Response({'code': 400, 'msg': '会员名称不能为空'})
|
||||
|
||||
try:
|
||||
jiage = Decimal(str(request.data.get('jiage')))
|
||||
guanshifc = Decimal(str(request.data.get('guanshifc')))
|
||||
zuzhangfc = Decimal(str(request.data.get('zuzhangfc')))
|
||||
except Exception:
|
||||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||||
|
||||
from jituan.services.club_member_admin import create_bundle_member_for_club
|
||||
|
||||
data, err = create_bundle_member_for_club(
|
||||
request,
|
||||
jieshao=jieshao,
|
||||
jiage=jiage,
|
||||
guanshifc=guanshifc,
|
||||
zuzhangfc=zuzhangfc,
|
||||
jtjieshao=(request.data.get('jtjieshao') or '').strip(),
|
||||
formal_days=int(request.data.get('formal_days') or 30),
|
||||
included_huiyuan_ids=request.data.get('included_huiyuan_ids') or [],
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 400, 'msg': err})
|
||||
return Response({'code': 0, 'msg': '总会员创建并上架成功', 'data': data})
|
||||
|
||||
|
||||
735
backend/views/penalties.py
Normal file
735
backend/views/penalties.py
Normal file
@@ -0,0 +1,735 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
class FaKuanTongJiView(APIView):
|
||||
"""
|
||||
罚款统计接口
|
||||
POST /houtai/htglyhqcfsltj
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
|
||||
try:
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
if not has_fadan_view_permission(permissions, username):
|
||||
return Response({'code': 403, 'msg': '无权限查看罚款统计'})
|
||||
|
||||
from utils.penalty_status import penalty_platform_audit_pending_q
|
||||
pending_q = penalty_platform_audit_pending_q()
|
||||
stats = filter_penalty_qs(Penalty.query.all(), request).aggregate(
|
||||
total=Count('id'),
|
||||
daijiaona=Count('id', filter=Q(Status=1)),
|
||||
shensuzhong=Count('id', filter=Q(Status=3)),
|
||||
yijiaona=Count('id', filter=Q(Status=2)),
|
||||
yibohui=Count('id', filter=Q(Status=4)),
|
||||
pingtai_shenhe=Count('id', filter=pending_q),
|
||||
)
|
||||
data = {k: v or 0 for k, v in stats.items()}
|
||||
from jituan.services.club_user_access import list_response_meta
|
||||
return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}})
|
||||
except Exception as e:
|
||||
logger.error(f"罚款统计异常: {traceback.format_exc()}")
|
||||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||
|
||||
|
||||
class FaKuanLieBiaoView(APIView):
|
||||
"""
|
||||
罚款列表接口
|
||||
POST /houtai/hthqfklb
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
|
||||
try:
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if not has_fadan_view_permission(permissions, username):
|
||||
return Response({'code': 403, 'msg': '无权限查看罚款列表'})
|
||||
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 20))
|
||||
if page_size > 100:
|
||||
page_size = 100
|
||||
|
||||
qs = filter_penalty_qs(Penalty.query.all(), request)
|
||||
|
||||
# 状态筛选(5=平台审核中,仅 zhuangtai=5)
|
||||
from utils.penalty_status import penalty_platform_audit_pending_q
|
||||
zhuangtai_raw = request.data.get('zhuangtai')
|
||||
if zhuangtai_raw is not None and str(zhuangtai_raw).strip() != '':
|
||||
try:
|
||||
zt = int(zhuangtai_raw)
|
||||
if zt == 5:
|
||||
qs = qs.filter(penalty_platform_audit_pending_q())
|
||||
elif zt in (1, 2, 3, 4):
|
||||
qs = qs.filter(Status=zt)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 被处罚人ID
|
||||
PenalizedUserID = request.data.get('PenalizedUserID', '').strip()
|
||||
if PenalizedUserID:
|
||||
qs = qs.filter(PenalizedUserID=PenalizedUserID)
|
||||
|
||||
# 被处罚人身份
|
||||
beichufa_shenfen = request.data.get('beichufa_shenfen')
|
||||
if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]:
|
||||
qs = qs.filter(Identity=int(beichufa_shenfen))
|
||||
|
||||
# 模糊搜索(订单ID、被罚人ID、申请人ID)
|
||||
sousuo = request.data.get('sousuo', '').strip()
|
||||
if sousuo:
|
||||
qs = qs.filter(
|
||||
Q(RelatedOrderID__icontains=sousuo) |
|
||||
Q(PenalizedUserID__icontains=sousuo) |
|
||||
Q(ApplicantID__icontains=sousuo)
|
||||
)
|
||||
|
||||
qs = qs.order_by('-CreateTime')
|
||||
paginator = Paginator(qs, page_size)
|
||||
try:
|
||||
page_obj = paginator.page(page)
|
||||
except Exception:
|
||||
return Response({
|
||||
'code': 0, 'msg': '成功',
|
||||
'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size}
|
||||
})
|
||||
|
||||
records = list(page_obj)
|
||||
penalty_ids = [r.id for r in records]
|
||||
|
||||
# 批量获取图片并分组
|
||||
zhengju_map = {}
|
||||
shensu_map = {}
|
||||
if penalty_ids:
|
||||
tupians = PenaltyAppealImage.query.filter(
|
||||
Penalty_id__in=penalty_ids
|
||||
).values('Penalty_id', 'ImageURL', 'Purpose')
|
||||
for t in tupians:
|
||||
fid = t['Penalty_id']
|
||||
url = t['ImageURL']
|
||||
if t['Purpose'] == 1:
|
||||
zhengju_map.setdefault(fid, []).append(url)
|
||||
else:
|
||||
shensu_map.setdefault(fid, []).append(url)
|
||||
|
||||
from utils.penalty_status import penalty_display_status, is_penalty_platform_audit_pending
|
||||
|
||||
# 组装列表数据
|
||||
results = []
|
||||
for r in records:
|
||||
results.append({
|
||||
'id': r.id,
|
||||
'beichufa_id': r.PenalizedUserID,
|
||||
'shenfen': r.Identity,
|
||||
'shenfen_text': dict(Penalty._meta.get_field('Identity').choices).get(r.Identity, ''),
|
||||
'shenqing_chufa': r.ApplicantID or '',
|
||||
'shenqingren_shenfen': r.ApplicantIdentity,
|
||||
'chufaliyou': r.Reason or '',
|
||||
'fakuanjine': str(r.FineAmount),
|
||||
'guanliandingdan_id': r.RelatedOrderID or '',
|
||||
'zhuangtai': r.Status,
|
||||
'zhuangtai_text': penalty_display_status(r),
|
||||
'platform_audit_pending': is_penalty_platform_audit_pending(r),
|
||||
'yingxiang_qiangdan': r.AffectsGrabbing,
|
||||
'shensuliyou': r.AppealReason or '',
|
||||
'bohuiliyou': r.RejectReason or '',
|
||||
'chulizhe': r.ProcessorID or '',
|
||||
'chulizhe_shenfen': r.ProcessorIdentity,
|
||||
'zhengju_tupian': zhengju_map.get(r.id, []),
|
||||
'shensu_tupian': shensu_map.get(r.id, []),
|
||||
'creat_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||||
'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||||
'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '',
|
||||
})
|
||||
|
||||
from jituan.services.club_user_access import list_response_meta
|
||||
return Response({
|
||||
'code': 0, 'msg': '成功',
|
||||
'data': {
|
||||
'list': results,
|
||||
'total': paginator.count,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
**list_response_meta(request),
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"罚款列表异常: {traceback.format_exc()}")
|
||||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||
|
||||
|
||||
class FaKuanChuLiView(APIView):
|
||||
"""
|
||||
处理罚款申诉(同意/拒绝)
|
||||
POST /houtai/glyclfk
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username = request.data.get('username', '').strip()
|
||||
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
|
||||
action = request.data.get('action', '').strip()
|
||||
chuli_liyou = request.data.get('chuli_liyou', '').strip()
|
||||
|
||||
if not all([username, penalty_id, action]):
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
|
||||
except Penalty.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '罚单不存在'})
|
||||
|
||||
deny = forbid_penalty_out_of_scope(request, penalty)
|
||||
if deny:
|
||||
return deny
|
||||
|
||||
if penalty.Status != 3:
|
||||
return Response({'code': 400, 'msg': '当前状态不可处理'})
|
||||
|
||||
if not check_fadan_permission(permissions, penalty.Identity):
|
||||
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
|
||||
|
||||
# 处理上传的图片
|
||||
uploaded_urls = []
|
||||
files = request.FILES.getlist('tupian') or []
|
||||
if not files:
|
||||
single = request.FILES.get('tupian')
|
||||
if single:
|
||||
files = [single]
|
||||
for file in files:
|
||||
valid, msg = validate_image(file)
|
||||
if not valid:
|
||||
return Response({'code': 400, 'msg': msg})
|
||||
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||||
file_name = f"chufa_{penalty_id}_{int(time.time() * 1000)}.{ext}"
|
||||
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||||
url = upload_to_oss(file, file_path)
|
||||
if not url:
|
||||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||||
uploaded_urls.append(file_path)
|
||||
|
||||
# 更新状态
|
||||
if action == 'agree': # 同意申诉 → 驳回处罚
|
||||
penalty.Status = 4
|
||||
penalty.RejectReason = chuli_liyou
|
||||
elif action == 'reject': # 拒绝申诉 → 回到待缴纳
|
||||
penalty.Status = 1
|
||||
penalty.RejectReason = chuli_liyou
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '无效的操作类型'})
|
||||
|
||||
# 记录处理者(使用 request.user 的 phone)
|
||||
penalty.ProcessorID = request.user.Phone
|
||||
penalty.ProcessorIdentity = 2 # 这里假设处理者是售后身份,可根据实际业务获取
|
||||
penalty.save()
|
||||
|
||||
# 保存图片(证据图片,用途1)
|
||||
for relative_url in uploaded_urls:
|
||||
PenaltyAppealImage.query.create(
|
||||
Penalty_id=penalty_id,
|
||||
PenalizedUserID=penalty.PenalizedUserID,
|
||||
ImageURL=relative_url,
|
||||
Purpose=1
|
||||
)
|
||||
|
||||
logger.info(f"罚款处理成功: {penalty_id}, 操作: {action}")
|
||||
return Response({'code': 0, 'msg': '处理成功'})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"罚款处理异常: {traceback.format_exc()}")
|
||||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||
|
||||
|
||||
class FaKuanPingTaiShenHeView(APIView):
|
||||
"""
|
||||
平台审核商家提交的罚款(同意/驳回)
|
||||
POST /houtai/glyptshfk
|
||||
action: platform_approve | platform_reject
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
from utils.penalty_status import (
|
||||
PENALTY_PLATFORM_AUDIT, PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_REJECTED,
|
||||
)
|
||||
username = request.data.get('username', '').strip()
|
||||
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
|
||||
action = request.data.get('action', '').strip()
|
||||
chuli_liyou = request.data.get('chuli_liyou', '').strip()
|
||||
|
||||
if not all([username, penalty_id, action]):
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
|
||||
except Penalty.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '罚单不存在'})
|
||||
|
||||
deny = forbid_penalty_out_of_scope(request, penalty)
|
||||
if deny:
|
||||
return deny
|
||||
|
||||
if penalty.Status != PENALTY_PLATFORM_AUDIT:
|
||||
return Response({'code': 400, 'msg': '当前状态不可平台审核'})
|
||||
|
||||
if not check_fadan_permission(permissions, penalty.Identity):
|
||||
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
|
||||
|
||||
if action == 'platform_approve':
|
||||
if penalty.AppealReason:
|
||||
penalty.Status = PENALTY_APPEALING
|
||||
else:
|
||||
penalty.Status = PENALTY_PENDING_PAY
|
||||
penalty.ApproveReason = chuli_liyou or penalty.ApproveReason or ''
|
||||
elif action == 'platform_reject':
|
||||
penalty.Status = PENALTY_REJECTED
|
||||
penalty.RejectReason = chuli_liyou or '平台审核驳回'
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '无效的操作类型'})
|
||||
|
||||
penalty.ProcessorID = request.user.Phone
|
||||
penalty.ProcessorIdentity = 3
|
||||
penalty.save()
|
||||
|
||||
logger.info(f"罚款平台审核成功: {penalty_id}, 操作: {action}")
|
||||
return Response({'code': 0, 'msg': '平台审核完成'})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"罚款平台审核异常: {traceback.format_exc()}")
|
||||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||
|
||||
|
||||
class FaKuanChuangJianView(APIView):
|
||||
"""
|
||||
管理员创建罚款
|
||||
POST /houtai/htfksc
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username = request.data.get('username', '').strip()
|
||||
PenalizedUserID, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
|
||||
shenfen = request.data.get('shenfen')
|
||||
|
||||
if not all([username, PenalizedUserID, shenfen, Reason, FineAmount is not None]):
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
|
||||
try:
|
||||
shenfen = int(shenfen)
|
||||
FineAmount = float(FineAmount)
|
||||
except (ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': '参数格式错误'})
|
||||
|
||||
if shenfen not in [1, 2, 3, 4]:
|
||||
return Response({'code': 400, 'msg': '无效的被罚人身份'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
if not check_fadan_permission(permissions, shenfen):
|
||||
return Response({'code': 403, 'msg': '无权限创建该类罚单'})
|
||||
|
||||
# 验证用户是否存在对应的扩展表
|
||||
try:
|
||||
user = User.query.get(UserUID=PenalizedUserID)
|
||||
profile_attr = SHENFEN_PROFILE_MAP.get(shenfen)
|
||||
if not profile_attr or not hasattr(user, profile_attr):
|
||||
return Response({'code': 400, 'msg': '该用户不是所选身份'})
|
||||
except User.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不存在'})
|
||||
|
||||
# 处理图片上传
|
||||
uploaded_urls = []
|
||||
files = request.FILES.getlist('tupian') or []
|
||||
if not files:
|
||||
single = request.FILES.get('tupian')
|
||||
if single:
|
||||
files = [single]
|
||||
for file in files:
|
||||
valid, msg = validate_image(file)
|
||||
if not valid:
|
||||
return Response({'code': 400, 'msg': msg})
|
||||
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||||
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
|
||||
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||||
url = upload_to_oss(file, file_path)
|
||||
if not url:
|
||||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||||
uploaded_urls.append(file_path)
|
||||
|
||||
with transaction.atomic():
|
||||
penalty = Penalty.query.create(
|
||||
PenalizedUserID=PenalizedUserID,
|
||||
Identity=shenfen,
|
||||
ApplicantID=request.user.Phone, # 申请人(客服)手机号
|
||||
ApplicantIdentity=2, # 售后身份
|
||||
Reason=Reason,
|
||||
FineAmount=FineAmount,
|
||||
AffectsGrabbing=AffectsGrabbing,
|
||||
Status=1, # 待缴纳
|
||||
ClubID=resolve_penalty_club_id(
|
||||
penalized_user_id=PenalizedUserID,
|
||||
request=request,
|
||||
user=request.user,
|
||||
),
|
||||
)
|
||||
PenaltyAppealImage.objects.bulk_create([
|
||||
PenaltyAppealImage(
|
||||
Penalty=penalty,
|
||||
PenalizedUserID=PenalizedUserID,
|
||||
ImageURL=relative_url,
|
||||
Purpose=1,
|
||||
)
|
||||
for relative_url in uploaded_urls
|
||||
])
|
||||
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||||
lock_penalty_bonus(penalty)
|
||||
|
||||
logger.info(f"罚款创建成功: {penalty.id}")
|
||||
return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': penalty.id}})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建罚款异常: {traceback.format_exc()}")
|
||||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||
|
||||
|
||||
class FineApplyView(APIView):
|
||||
"""
|
||||
客服罚款申请接口(基于订单对打手发起罚款)
|
||||
POST /houtai/kffkdssq
|
||||
权限: JWT认证 + 客服权限验证
|
||||
|
||||
请求参数:
|
||||
username (必填) : 客服用户名(手机号)
|
||||
OrderID (必填) : 订单ID
|
||||
Reason (必填) : 罚款原因
|
||||
FineAmount (必填) : 罚款金额(元)
|
||||
AffectsGrabbing (可选) : 是否影响抢单,1=是,0=否,默认1
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username = request.data.get('username', '').strip()
|
||||
OrderID = pick_order_id(request.data)
|
||||
_, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
|
||||
|
||||
if not all([username, OrderID, Reason, FineAmount is not None]):
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
|
||||
try:
|
||||
FineAmount = float(FineAmount)
|
||||
except (ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': '罚款金额格式错误'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
# 验证订单存在
|
||||
try:
|
||||
order = Order.query.get(OrderID=OrderID)
|
||||
except Order.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '订单不存在'})
|
||||
|
||||
# 获取打手ID
|
||||
dashou_id = order.PlayerID
|
||||
if not dashou_id:
|
||||
return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
|
||||
|
||||
# 重复罚款检查
|
||||
if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=OrderID).exists():
|
||||
return Response({'code': 400, 'msg': '该打手已被罚款过'})
|
||||
|
||||
# 检查权限(打手身份 shenfen=1)
|
||||
if not check_fadan_permission(permissions, 1):
|
||||
return Response({'code': 403, 'msg': '无权限处罚打手'})
|
||||
|
||||
# 处理图片上传
|
||||
uploaded_urls = []
|
||||
files = request.FILES.getlist('tupian') or []
|
||||
if not files:
|
||||
single = request.FILES.get('tupian')
|
||||
if single:
|
||||
files = [single]
|
||||
for file in files:
|
||||
valid, msg = validate_image(file)
|
||||
if not valid:
|
||||
return Response({'code': 400, 'msg': msg})
|
||||
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||||
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
|
||||
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||||
url = upload_to_oss(file, file_path)
|
||||
if not url:
|
||||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||||
uploaded_urls.append(file_path)
|
||||
|
||||
# 创建罚单
|
||||
with transaction.atomic():
|
||||
penalty = Penalty.query.create(
|
||||
PenalizedUserID=dashou_id,
|
||||
ApplicantID=request.user.Phone,
|
||||
Identity=1, # 被处罚人身份:1 打手
|
||||
Reason=Reason,
|
||||
FineAmount=FineAmount,
|
||||
RelatedOrderID=OrderID,
|
||||
Status=1, # 待缴纳
|
||||
AffectsGrabbing=AffectsGrabbing,
|
||||
ApplicantIdentity=1, # 申请人身份:1 客服
|
||||
ClubID=resolve_penalty_club_id(
|
||||
order=order,
|
||||
request=request,
|
||||
user=request.user,
|
||||
applicant_id=getattr(request.user, 'UserUID', None),
|
||||
),
|
||||
)
|
||||
for relative_url in uploaded_urls:
|
||||
PenaltyAppealImage.query.create(
|
||||
Penalty=penalty,
|
||||
PenalizedUserID=dashou_id,
|
||||
ImageURL=relative_url,
|
||||
Purpose=1
|
||||
)
|
||||
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||||
lock_penalty_bonus(penalty)
|
||||
|
||||
logger.info(f"客服罚款申请成功: 订单 {OrderID}, 打手 {dashou_id}, 金额 {FineAmount}")
|
||||
return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': penalty.id}})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"客服罚款申请异常: {traceback.format_exc()}")
|
||||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||
|
||||
|
||||
class PunishDashouView(APIView):
|
||||
"""
|
||||
客服处罚打手接口(扣除积分)
|
||||
POST /houtai/kpcf
|
||||
权限: JWT认证 + 客服权限验证
|
||||
|
||||
请求参数:
|
||||
username (必填) : 客服用户名(手机号)
|
||||
OrderID (必填) : 订单ID
|
||||
reason (可选) : 处罚原因
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username = request.data.get('username', '').strip()
|
||||
OrderID = request.data.get('OrderID', '').strip()
|
||||
reason = request.data.get('reason', '').strip()
|
||||
|
||||
if not all([username, OrderID]):
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
# 检查权限(打手身份 shenfen=1)
|
||||
if not check_fadan_permission(permissions, 1):
|
||||
return Response({'code': 403, 'msg': '无权限处罚打手'})
|
||||
|
||||
# 查询订单
|
||||
try:
|
||||
order = Order.query.get(OrderID=OrderID)
|
||||
except Order.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '订单不存在'})
|
||||
|
||||
# 获取打手ID
|
||||
dashou_id = order.PlayerID
|
||||
if not dashou_id:
|
||||
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'})
|
||||
|
||||
# 查询打手
|
||||
try:
|
||||
dashou_user = User.query.get(UserUID=dashou_id)
|
||||
dashou = dashou_user.DashouProfile
|
||||
except User.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '打手用户不存在'})
|
||||
except ObjectDoesNotExist:
|
||||
return Response({'code': 400, 'msg': '该用户不是打手'})
|
||||
|
||||
# 创建处罚记录(扣除积分5分)
|
||||
jifen = 5
|
||||
with transaction.atomic():
|
||||
penalty_record = PenaltyRecord.query.create(
|
||||
PlayerID=dashou_id,
|
||||
ApplicantID=username,
|
||||
PenaltyReason=reason or '',
|
||||
ApplyStatus=0,
|
||||
DeductedPoints=jifen,
|
||||
OrderID=OrderID,
|
||||
ClubID=resolve_penalty_record_club_id(
|
||||
order_id=OrderID,
|
||||
player_id=dashou_id,
|
||||
request=request,
|
||||
user=request.user,
|
||||
applicant_id=getattr(request.user, 'UserUID', None) or username,
|
||||
),
|
||||
)
|
||||
# 扣除打手积分
|
||||
dashou.jifen = F('jifen') - jifen
|
||||
dashou.save(update_fields=['jifen'])
|
||||
|
||||
logger.info(f"客服处罚打手成功: 打手 {dashou_id}, 订单 {OrderID}, 扣除积分 {jifen}")
|
||||
return Response({'code': 0, 'msg': '处罚成功', 'data': {'penalty_record_id': penalty_record.id}})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"客服处罚打手异常: {traceback.format_exc()}")
|
||||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1151
backend/views/products.py
Normal file
1151
backend/views/products.py
Normal file
File diff suppressed because it is too large
Load Diff
960
backend/views/roles.py
Normal file
960
backend/views/roles.py
Normal file
@@ -0,0 +1,960 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
def _serialize_permission_row(perm_row):
|
||||
"""gvsdsdk Permission → 后台 Vue 约定的小写字段"""
|
||||
if isinstance(perm_row, dict):
|
||||
code = perm_row.get('PermCode') or perm_row.get('perm_code') or ''
|
||||
name = perm_row.get('PermName') or perm_row.get('perm_name') or code
|
||||
desc = perm_row.get('PermDesc') or perm_row.get('perm_desc') or ''
|
||||
else:
|
||||
code = perm_row.PermCode or ''
|
||||
name = perm_row.PermName or code
|
||||
desc = perm_row.PermDesc or ''
|
||||
return {'perm_code': code, 'perm_name': name, 'perm_desc': desc}
|
||||
|
||||
|
||||
class GetRolePermissionView(APIView):
|
||||
"""
|
||||
获取所有角色、权限以及角色关联的用户数
|
||||
权限要求:用户必须拥有 000001 权限
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 400, 'msg': '缺少username'})
|
||||
|
||||
# 1. 权限校验(验证客服身份,并获取权限列表)
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions # 直接返回错误响应
|
||||
|
||||
from jituan.services.admin_context import can_access_admin_user_management
|
||||
if not can_access_admin_user_management(request.user, permissions):
|
||||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||||
|
||||
# 3. 获取所有权限
|
||||
all_perms = Permission.objects.filter(PermStatus=1).values('PermCode', 'PermName', 'PermDesc')
|
||||
permissions_list = [_serialize_permission_row(p) for p in all_perms]
|
||||
|
||||
from jituan.services.club_rbac import count_role_users_in_scope, filter_roles_for_request
|
||||
|
||||
kefu_user_uuids = list(
|
||||
User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True)
|
||||
)
|
||||
roles = list(filter_roles_for_request(request))
|
||||
# 批量预取 RolePermission + Permission(避免循环内 N+1)
|
||||
_role_uuids = [r.RoleUUID for r in roles]
|
||||
_rp_pairs = list(
|
||||
RolePermission.objects.filter(RoleUUID__in=_role_uuids)
|
||||
.values_list('RoleUUID', 'PermUUID')
|
||||
) if _role_uuids else []
|
||||
_perm_uuids = list({p for _, p in _rp_pairs})
|
||||
_perm_rows_by_uuid = {
|
||||
p['PermUUID']: p
|
||||
for p in Permission.objects.filter(PermUUID__in=_perm_uuids)
|
||||
.values('PermUUID', 'PermCode', 'PermName', 'PermDesc')
|
||||
} if _perm_uuids else {}
|
||||
_perms_by_role = {}
|
||||
for role_uuid, perm_uuid in _rp_pairs:
|
||||
perm_row = _perm_rows_by_uuid.get(perm_uuid)
|
||||
if perm_row is not None:
|
||||
_perms_by_role.setdefault(role_uuid, []).append(_serialize_permission_row(perm_row))
|
||||
|
||||
roles_data = []
|
||||
for role in roles:
|
||||
user_count = count_role_users_in_scope(role, request, kefu_user_uuids)
|
||||
perms_of_role = _perms_by_role.get(role.RoleUUID, [])
|
||||
roles_data.append({
|
||||
'role_code': role.RoleName,
|
||||
'role_name': role.RoleName,
|
||||
'description': role.RoleDesc or '',
|
||||
'club_id': getattr(role, 'ClubID', '') or '',
|
||||
'user_count': user_count,
|
||||
'permissions': perms_of_role
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'roles': roles_data,
|
||||
'permissions': permissions_list
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class ModifyRolePermissionView(APIView):
|
||||
"""
|
||||
通用接口:支持删除角色、为角色添加权限、移除角色权限
|
||||
操作类型通过 action 字段区分
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
action = request.data.get('action') # delete_role, add_permission, remove_permission
|
||||
if not username or not action:
|
||||
return Response({'code': 400, 'msg': '缺少必要参数'})
|
||||
|
||||
# 权限校验
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '000001' not in permissions:
|
||||
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||||
|
||||
# 获取角色(按当前俱乐部解析,避免多俱乐部同名角色冲突)
|
||||
role_code = request.data.get('role_code')
|
||||
if not role_code:
|
||||
return Response({'code': 400, 'msg': '缺少角色编码'})
|
||||
|
||||
from jituan.services.club_rbac import resolve_role_by_name, resolve_role_club_id
|
||||
explicit_club = (request.data.get('target_club_id') or request.data.get('club_id') or '').strip() or None
|
||||
bind_club_id = resolve_role_club_id(request, explicit_club)
|
||||
role = resolve_role_by_name(role_code, bind_club_id)
|
||||
if not role:
|
||||
return Response({'code': 404, 'msg': '角色不存在或与当前俱乐部不匹配'})
|
||||
|
||||
# 超级管理员角色的特殊限制
|
||||
if role.RoleName == '管理员':
|
||||
if action in ['delete_role']:
|
||||
return Response({'code': 403, 'msg': '超级管理员角色不能删除'})
|
||||
|
||||
# 根据 action 处理
|
||||
if action == 'delete_role':
|
||||
# 删除角色:同时删除角色权限关联、用户角色关联
|
||||
with transaction.atomic():
|
||||
RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete()
|
||||
UserRole.objects.filter(RoleUUID=role.RoleUUID).delete()
|
||||
role.delete()
|
||||
return Response({'code': 0, 'msg': '角色删除成功'})
|
||||
|
||||
elif action == 'add_permission':
|
||||
perm_code = request.data.get('perm_code')
|
||||
if not perm_code:
|
||||
return Response({'code': 400, 'msg': '缺少权限编码'})
|
||||
if role.RoleName == '管理员' and perm_code == '000001':
|
||||
return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'})
|
||||
try:
|
||||
perm = Permission.objects.get(PermCode=perm_code)
|
||||
except Permission.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '权限不存在'})
|
||||
# 检查是否已存在
|
||||
if RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).exists():
|
||||
return Response({'code': 400, 'msg': '该角色已拥有此权限'})
|
||||
RolePermission.objects.create(
|
||||
RolePermUUID=uuid.uuid4().bytes,
|
||||
RoleUUID=role.RoleUUID,
|
||||
PermUUID=perm.PermUUID,
|
||||
CreateTime=timezone.now(),
|
||||
)
|
||||
return Response({'code': 0, 'msg': '权限添加成功'})
|
||||
|
||||
elif action == 'remove_permission':
|
||||
perm_code = request.data.get('perm_code')
|
||||
if not perm_code:
|
||||
return Response({'code': 400, 'msg': '缺少权限编码'})
|
||||
if role.RoleName == '管理员' and perm_code == '000001':
|
||||
return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'})
|
||||
try:
|
||||
perm = Permission.objects.get(PermCode=perm_code)
|
||||
except Permission.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '权限不存在'})
|
||||
deleted, _ = RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).delete()
|
||||
if deleted == 0:
|
||||
return Response({'code': 400, 'msg': '该角色未拥有此权限'})
|
||||
return Response({'code': 0, 'msg': '权限移除成功'})
|
||||
|
||||
elif action == 'update_role':
|
||||
role_name = (request.data.get('role_name') or '').strip()
|
||||
description = (request.data.get('description') or '').strip()
|
||||
perm_codes = request.data.get('perm_codes') or []
|
||||
if role.RoleName == '管理员':
|
||||
role_name = role.RoleName
|
||||
elif role_name and role_name != role.RoleName:
|
||||
role_club_id = getattr(role, 'ClubID', '') or ''
|
||||
if Role.objects.filter(
|
||||
RoleName=role_name, ClubID=role_club_id,
|
||||
).exclude(RoleUUID=role.RoleUUID).exists():
|
||||
return Response({'code': 400, 'msg': '该俱乐部下角色名称已存在'})
|
||||
with transaction.atomic():
|
||||
if role.RoleName != '管理员':
|
||||
if role_name:
|
||||
role.RoleName = role_name
|
||||
role.RoleDesc = description
|
||||
role.save(update_fields=['RoleName', 'RoleDesc'])
|
||||
RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete()
|
||||
for pc in perm_codes:
|
||||
if pc == '000001' and role.RoleName != '管理员':
|
||||
continue
|
||||
try:
|
||||
perm = Permission.objects.get(PermCode=pc, PermStatus=1)
|
||||
except Permission.DoesNotExist:
|
||||
continue
|
||||
RolePermission.objects.create(
|
||||
RolePermUUID=uuid.uuid4().bytes,
|
||||
RoleUUID=role.RoleUUID,
|
||||
PermUUID=perm.PermUUID,
|
||||
CreateTime=timezone.now(),
|
||||
)
|
||||
if role.RoleName == '管理员':
|
||||
try:
|
||||
super_perm = Permission.objects.get(PermCode='000001', PermStatus=1)
|
||||
if not RolePermission.objects.filter(
|
||||
RoleUUID=role.RoleUUID, PermUUID=super_perm.PermUUID,
|
||||
).exists():
|
||||
RolePermission.objects.create(
|
||||
RolePermUUID=uuid.uuid4().bytes,
|
||||
RoleUUID=role.RoleUUID,
|
||||
PermUUID=super_perm.PermUUID,
|
||||
CreateTime=timezone.now(),
|
||||
)
|
||||
except Permission.DoesNotExist:
|
||||
pass
|
||||
return Response({'code': 0, 'msg': '角色保存成功'})
|
||||
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '无效的 action'})
|
||||
|
||||
|
||||
class AddRoleView(APIView):
|
||||
"""
|
||||
添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
role_name = request.data.get('role_name', '').strip()
|
||||
description = request.data.get('description', '').strip()
|
||||
perm_codes = request.data.get('perm_codes', []) # 列表
|
||||
|
||||
if not username or not role_name:
|
||||
return Response({'code': 400, 'msg': '缺少必要参数'})
|
||||
|
||||
# 权限校验
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '000001' not in permissions:
|
||||
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||||
|
||||
# 检查角色名称是否已存在(同俱乐部内唯一)
|
||||
from jituan.services.club_rbac import resolve_role_club_id, role_name_exists
|
||||
target_club_id = resolve_role_club_id(
|
||||
request, (request.data.get('target_club_id') or '').strip() or None,
|
||||
)
|
||||
if role_name_exists(role_name, target_club_id):
|
||||
return Response({'code': 400, 'msg': '该俱乐部下角色名称已存在'})
|
||||
|
||||
# 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字)
|
||||
role_code = f"ROLE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=8))}"
|
||||
|
||||
# 创建角色
|
||||
with transaction.atomic():
|
||||
role = Role.objects.create(
|
||||
RoleUUID=uuid.uuid4().bytes,
|
||||
TenantUUID=b'\x00' * 16,
|
||||
RoleName=role_name,
|
||||
RoleType=2,
|
||||
RoleStatus=1,
|
||||
AssignScope=0,
|
||||
RoleDesc=description,
|
||||
ClubID=target_club_id,
|
||||
CreateTime=timezone.now(),
|
||||
)
|
||||
# 绑定权限(过滤掉不存在的权限编码)
|
||||
for pc in perm_codes:
|
||||
# 不允许添加 000001 权限(此权限仅供超级管理员)
|
||||
if pc == '000001':
|
||||
continue
|
||||
try:
|
||||
perm = Permission.objects.get(PermCode=pc)
|
||||
RolePermission.objects.create(
|
||||
RolePermUUID=uuid.uuid4().bytes,
|
||||
RoleUUID=role.RoleUUID,
|
||||
PermUUID=perm.PermUUID,
|
||||
CreateTime=timezone.now(),
|
||||
)
|
||||
except Permission.DoesNotExist:
|
||||
logger.warning(f"添加角色时忽略不存在的权限编码: {pc}")
|
||||
|
||||
return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}})
|
||||
|
||||
|
||||
|
||||
class GetAdminUserListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 400, 'msg': '缺少username'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
from jituan.services.admin_context import can_access_admin_user_management
|
||||
if not can_access_admin_user_management(request.user, permissions):
|
||||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||||
|
||||
phone = request.data.get('phone', '').strip()
|
||||
nicheng = request.data.get('nicheng', '').strip()
|
||||
role_code = request.data.get('roleCode', '').strip()
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('pageSize', 20))
|
||||
|
||||
# UserType 是 @property 非数据库字段,不能用 filter;
|
||||
# KefuProfile__isnull=False 已确保只查到客服用户
|
||||
users = User.query.filter(
|
||||
KefuProfile__isnull=False
|
||||
).select_related('KefuProfile')
|
||||
|
||||
from jituan.models import AdminAssignment, Club
|
||||
from jituan.constants import ADMIN_ROLE_LABELS
|
||||
from jituan.services.club_rbac import filter_kefu_users_by_admin_scope
|
||||
users = filter_kefu_users_by_admin_scope(users, request)
|
||||
|
||||
if phone:
|
||||
users = users.filter(Phone__icontains=phone)
|
||||
if nicheng:
|
||||
users = users.filter(KefuProfile__nicheng__icontains=nicheng)
|
||||
|
||||
# 关键修正:先求值为列表,再传入 phone__in
|
||||
if role_code:
|
||||
from jituan.services.club_rbac import resolve_role_by_name, resolve_role_club_id
|
||||
filter_club_id = resolve_role_club_id(request)
|
||||
target_role = resolve_role_by_name(role_code, filter_club_id)
|
||||
if target_role:
|
||||
user_uuids = list(
|
||||
UserRole.objects.filter(RoleUUID=target_role.RoleUUID)
|
||||
.values_list('UserUUID', flat=True)
|
||||
)
|
||||
users = users.filter(UserUUID__in=user_uuids)
|
||||
else:
|
||||
users = users.none()
|
||||
|
||||
users = users.order_by('-UserCreateTime')
|
||||
paginator = Paginator(users, page_size)
|
||||
page_obj = paginator.get_page(page)
|
||||
|
||||
# 批量预取本页所有用户的 UserRole + Role(避免循环内 N+1)
|
||||
_page_users = list(page_obj)
|
||||
_page_user_uuids = [u.UserUUID for u in _page_users]
|
||||
_ur_pairs = list(
|
||||
UserRole.objects.filter(UserUUID__in=_page_user_uuids)
|
||||
.values_list('UserUUID', 'RoleUUID')
|
||||
) if _page_user_uuids else []
|
||||
_role_uuids_in_page = list({r for _, r in _ur_pairs})
|
||||
_role_info_by_uuid = {
|
||||
r.RoleUUID: {'role_name': r.RoleName, 'club_id': getattr(r, 'ClubID', '') or ''}
|
||||
for r in Role.objects.filter(RoleUUID__in=_role_uuids_in_page)
|
||||
} if _role_uuids_in_page else {}
|
||||
_roles_by_user = {}
|
||||
for user_uuid, role_uuid in _ur_pairs:
|
||||
info = _role_info_by_uuid.get(role_uuid)
|
||||
if info is not None:
|
||||
_roles_by_user.setdefault(user_uuid, []).append({
|
||||
'role_code': info['role_name'],
|
||||
'role_name': info['role_name'],
|
||||
'club_id': info['club_id'],
|
||||
})
|
||||
|
||||
_page_yonghuids = [u.UserUID for u in _page_users]
|
||||
_club_name_map = {c.club_id: c.name for c in Club.query.filter(status=1)}
|
||||
_assignments_by_user = {}
|
||||
if _page_yonghuids:
|
||||
for a in AdminAssignment.query.filter(yonghuid__in=_page_yonghuids, status=1):
|
||||
if a.club_id:
|
||||
club_label = _club_name_map.get(a.club_id, a.club_id)
|
||||
else:
|
||||
club_label = '集团(全部子公司)'
|
||||
_assignments_by_user.setdefault(a.yonghuid, []).append({
|
||||
'id': a.id,
|
||||
'club_id': a.club_id,
|
||||
'club_label': club_label,
|
||||
'role_code': a.role_code,
|
||||
'role_name': ADMIN_ROLE_LABELS.get(a.role_code, a.role_code),
|
||||
'is_primary': a.is_primary,
|
||||
})
|
||||
|
||||
data_list = []
|
||||
for user in _page_users:
|
||||
roles_data = _roles_by_user.get(user.UserUUID, [])
|
||||
data_list.append({
|
||||
'phone': user.Phone or '',
|
||||
'yonghuid': user.UserUID or '',
|
||||
'nicheng': user.KefuProfile.nicheng,
|
||||
'club_id': getattr(user, 'ClubID', '') or '',
|
||||
'roles': roles_data,
|
||||
'assignments': _assignments_by_user.get(user.UserUID, []),
|
||||
'status': user.KefuProfile.zhuangtai,
|
||||
'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
|
||||
'update_time': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '',
|
||||
'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
|
||||
'UpdateTime': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '',
|
||||
})
|
||||
|
||||
from jituan.services.admin_context import can_manage_admin_assignments, get_manageable_club_ids
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'list': data_list,
|
||||
'total': paginator.count,
|
||||
'page': page,
|
||||
'pageSize': page_size,
|
||||
'can_manage_club_assignments': can_manage_admin_assignments(request.user, permissions),
|
||||
'manageable_club_ids': sorted(get_manageable_club_ids(request.user, permissions)),
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}")
|
||||
return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'})
|
||||
|
||||
|
||||
|
||||
class GetAdminRolesView(APIView):
|
||||
"""获取所有角色(用于筛选和分配)"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 400, 'msg': '缺少username'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
from jituan.services.admin_context import can_access_admin_user_management
|
||||
if not can_access_admin_user_management(request.user, permissions):
|
||||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||||
|
||||
from jituan.services.admin_context import can_manage_admin_assignments, get_manageable_club_ids
|
||||
from jituan.services.club_rbac import filter_roles_for_request
|
||||
all_clubs = request.data.get('all_clubs') in (True, 1, '1', 'true', 'yes')
|
||||
if all_clubs and can_manage_admin_assignments(request.user, permissions):
|
||||
roles = Role.objects.filter(RoleStatus=1).order_by('ClubID', 'RoleName').values('RoleName', 'ClubID')
|
||||
else:
|
||||
manageable = get_manageable_club_ids(request.user, permissions)
|
||||
if manageable:
|
||||
roles = Role.objects.filter(
|
||||
RoleStatus=1, ClubID__in=manageable,
|
||||
).order_by('ClubID', 'RoleName').values('RoleName', 'ClubID')
|
||||
else:
|
||||
roles = filter_roles_for_request(request).values('RoleName', 'ClubID')
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'roles': [
|
||||
{
|
||||
'role_code': r['RoleName'],
|
||||
'role_name': r['RoleName'],
|
||||
'club_id': r.get('ClubID') or '',
|
||||
}
|
||||
for r in roles
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'''class GetAdminUserListView(APIView):
|
||||
"""获取后台用户列表(支持分页、筛选)"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 400, 'msg': '缺少username'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
from jituan.services.admin_context import can_access_admin_user_management
|
||||
if not can_access_admin_user_management(request.user, permissions):
|
||||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||||
|
||||
# 筛选条件
|
||||
phone = request.data.get('phone', '').strip()
|
||||
nicheng = request.data.get('nicheng', '').strip()
|
||||
role_code = request.data.get('roleCode', '').strip()
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('pageSize', 20))
|
||||
|
||||
# 基础查询:存在客服扩展表
|
||||
users = User.query.filter(KefuProfile__isnull=False).select_related('KefuProfile')
|
||||
if phone:
|
||||
users = users.filter(Phone__icontains=phone)
|
||||
if nicheng:
|
||||
users = users.filter(KefuProfile__nicheng__icontains=nicheng)
|
||||
if role_code:
|
||||
# 筛选拥有该角色的用户
|
||||
user_ids_with_role = UserRole.query.filter(role__role_code=role_code).values_list('account_id', flat=True)
|
||||
users = users.filter(phone__in=user_ids_with_role)
|
||||
|
||||
# 排序
|
||||
users = users.order_by('-CreateTime')
|
||||
|
||||
# 分页
|
||||
paginator = Paginator(users, page_size)
|
||||
page_obj = paginator.get_page(page)
|
||||
|
||||
# 构建返回数据
|
||||
data_list = []
|
||||
for user in page_obj:
|
||||
# 获取用户的所有角色
|
||||
user_roles = UserRole.query.filter(account_id=user.Phone).select_related('role')
|
||||
roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles]
|
||||
data_list.append({
|
||||
'phone': user.Phone,
|
||||
'nicheng': user.KefuProfile.nicheng,
|
||||
'roles': roles_data,
|
||||
'status': user.KefuProfile.zhuangtai,
|
||||
'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'UpdateTime': user.UpdateTime.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'list': data_list,
|
||||
'total': paginator.count,
|
||||
'page': page,
|
||||
'pageSize': page_size
|
||||
}
|
||||
})'''
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ModifyAdminUserView(APIView):
|
||||
"""修改后台用户信息(昵称、密码、二级密码、状态、角色)"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
action = request.data.get('action')
|
||||
if not username or not action:
|
||||
return Response({'code': 400, 'msg': '缺少参数'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
from jituan.services.admin_context import (
|
||||
can_manage_admin_assignments,
|
||||
can_manage_roles_for_club,
|
||||
can_access_admin_user_management,
|
||||
)
|
||||
|
||||
club_assign_actions = {'add_club_assignment', 'remove_club_assignment'}
|
||||
role_manage_actions = {'add_user_roles', 'remove_user_role'}
|
||||
if action in club_assign_actions:
|
||||
if not can_manage_admin_assignments(request.user, permissions):
|
||||
return Response({'code': 403, 'msg': '无权分配俱乐部任职'})
|
||||
elif action in role_manage_actions:
|
||||
if not can_access_admin_user_management(request.user, permissions):
|
||||
return Response({'code': 403, 'msg': '您无权管理角色分配'})
|
||||
elif not can_manage_admin_assignments(request.user, permissions) and '000001' not in permissions:
|
||||
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||||
|
||||
target_yonghuid = request.data.get('yonghuid', '').strip()
|
||||
target_phone = request.data.get('phone', '').strip()
|
||||
if not target_yonghuid and not target_phone:
|
||||
return Response({'code': 400, 'msg': '缺少目标用户(yonghuid 或 phone)'})
|
||||
|
||||
from jituan.services.club_rbac import resolve_kefu_admin_user
|
||||
target_user = resolve_kefu_admin_user(yonghuid=target_yonghuid, phone=target_phone)
|
||||
if not target_user:
|
||||
return Response({'code': 404, 'msg': '用户不存在或非后台账号'})
|
||||
try:
|
||||
target_kefu = target_user.KefuProfile
|
||||
except UserKefu.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户扩展信息缺失'})
|
||||
|
||||
with transaction.atomic():
|
||||
if action == 'update_user_info':
|
||||
# 修改昵称、状态、密码、二级密码
|
||||
nicheng = request.data.get('nicheng')
|
||||
status = request.data.get('status')
|
||||
password = request.data.get('password')
|
||||
erjimima = request.data.get('erjimima')
|
||||
if nicheng is not None:
|
||||
target_kefu.nicheng = nicheng
|
||||
if status is not None:
|
||||
target_kefu.zhuangtai = int(status)
|
||||
if password:
|
||||
if len(password) < 6:
|
||||
return Response({'code': 400, 'msg': '密码至少6位'})
|
||||
target_user.SetPassword(password)
|
||||
if erjimima:
|
||||
if len(erjimima) < 6:
|
||||
return Response({'code': 400, 'msg': '二级密码至少6位'})
|
||||
import bcrypt as _bcrypt
|
||||
target_kefu.erjimima = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
|
||||
target_user.save()
|
||||
target_kefu.save()
|
||||
return Response({'code': 0, 'msg': '修改成功'})
|
||||
|
||||
elif action == 'add_user_roles':
|
||||
role_codes = request.data.get('role_codes', [])
|
||||
if not role_codes:
|
||||
return Response({'code': 400, 'msg': '请选择角色'})
|
||||
from jituan.services.club_rbac import resolve_role_by_name
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
bind_club_id = (request.data.get('target_club_id') or '').strip()
|
||||
if not bind_club_id:
|
||||
bind_club_id = resolve_club_id_from_request(request)
|
||||
if not can_manage_roles_for_club(request.user, permissions, bind_club_id):
|
||||
return Response({'code': 403, 'msg': '您无权管理该俱乐部的角色分配'})
|
||||
added, missing = [], []
|
||||
for rc in role_codes:
|
||||
role = resolve_role_by_name(rc, bind_club_id)
|
||||
if not role:
|
||||
missing.append(rc)
|
||||
continue
|
||||
role_club = (getattr(role, 'ClubID', None) or '').strip()
|
||||
if role_club and role_club != bind_club_id:
|
||||
missing.append(rc)
|
||||
continue
|
||||
_, created = UserRole.objects.get_or_create(
|
||||
UserUUID=target_user.UserUUID,
|
||||
RoleUUID=role.RoleUUID,
|
||||
defaults={
|
||||
'UserRoleUUID': uuid.uuid4().bytes,
|
||||
'CompanyUUID': uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
added.append(role.RoleName)
|
||||
elif role.RoleName not in added:
|
||||
added.append(role.RoleName)
|
||||
if not added:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'msg': f'未找到可绑定的角色:{", ".join(missing or role_codes)}。请确认顶栏俱乐部与角色所属俱乐部一致',
|
||||
})
|
||||
if missing:
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': f'已绑定:{", ".join(added)};未找到:{", ".join(missing)}',
|
||||
})
|
||||
return Response({'code': 0, 'msg': '角色添加成功'})
|
||||
|
||||
elif action == 'remove_user_role':
|
||||
role_code = request.data.get('role_code')
|
||||
if not role_code:
|
||||
return Response({'code': 400, 'msg': '缺少角色编码'})
|
||||
from jituan.services.club_rbac import resolve_role_by_name
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
bind_club_id = (request.data.get('target_club_id') or '').strip()
|
||||
if not bind_club_id:
|
||||
bind_club_id = resolve_club_id_from_request(request)
|
||||
if not can_manage_roles_for_club(request.user, permissions, bind_club_id):
|
||||
return Response({'code': 403, 'msg': '您无权管理该俱乐部的角色分配'})
|
||||
role = resolve_role_by_name(role_code, bind_club_id)
|
||||
if not role:
|
||||
return Response({'code': 404, 'msg': '角色不存在或与所选俱乐部不匹配'})
|
||||
UserRole.objects.filter(UserUUID=target_user.UserUUID, RoleUUID=role.RoleUUID).delete()
|
||||
return Response({'code': 0, 'msg': '角色移除成功'})
|
||||
|
||||
elif action == 'add_club_assignment':
|
||||
club_id = (request.data.get('club_id') or '').strip()
|
||||
if not club_id:
|
||||
return Response({'code': 400, 'msg': '请选择俱乐部'})
|
||||
from jituan.services.admin_assignments import create_or_reactivate_assignment
|
||||
from jituan.constants import DATA_SCOPE_SINGLE
|
||||
role_code = (request.data.get('role_code') or 'CLUB_ADMIN').strip()
|
||||
is_primary = bool(request.data.get('is_primary', False))
|
||||
_, err = create_or_reactivate_assignment(
|
||||
yonghuid=target_user.UserUID,
|
||||
club_id=club_id,
|
||||
role_code=role_code,
|
||||
data_scope=DATA_SCOPE_SINGLE,
|
||||
is_primary=is_primary,
|
||||
granted_by=request.user.UserUID,
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 400, 'msg': err})
|
||||
return Response({'code': 0, 'msg': '俱乐部任职已添加'})
|
||||
|
||||
elif action == 'remove_club_assignment':
|
||||
assignment_id = request.data.get('assignment_id')
|
||||
if not assignment_id:
|
||||
return Response({'code': 400, 'msg': '缺少任职 id'})
|
||||
from jituan.services.admin_assignments import deactivate_assignment
|
||||
from jituan.models import AdminAssignment
|
||||
row = AdminAssignment.query.filter(
|
||||
id=assignment_id, yonghuid=target_user.UserUID, status=1,
|
||||
).first()
|
||||
if not row:
|
||||
return Response({'code': 404, 'msg': '任职记录不存在'})
|
||||
ok, err = deactivate_assignment(assignment_id)
|
||||
if not ok:
|
||||
return Response({'code': 404, 'msg': err or '停用失败'})
|
||||
return Response({'code': 0, 'msg': '俱乐部任职已移除'})
|
||||
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '无效的action'})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class AddAdminUserView(APIView):
|
||||
"""添加后台用户(客服账号)"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 400, 'msg': '缺少username'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
from jituan.services.admin_context import (
|
||||
can_manage_admin_assignments,
|
||||
can_manage_roles_for_club,
|
||||
)
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
|
||||
target_club_id = resolve_club_id_from_request(request)
|
||||
if not (
|
||||
can_manage_admin_assignments(request.user, permissions)
|
||||
or can_manage_roles_for_club(request.user, permissions, target_club_id)
|
||||
):
|
||||
return Response({'code': 403, 'msg': '您无权在该俱乐部添加用户'})
|
||||
|
||||
phone = request.data.get('phone', '').strip()
|
||||
password = request.data.get('password', '').strip()
|
||||
erjimima = request.data.get('erjimima', '').strip()
|
||||
nicheng = request.data.get('nicheng', '').strip()
|
||||
role_codes = request.data.get('role_codes', [])
|
||||
|
||||
if not all([phone, password, erjimima, nicheng]):
|
||||
return Response({'code': 400, 'msg': '请填写完整信息'})
|
||||
if len(password) < 6 or len(erjimima) < 6:
|
||||
return Response({'code': 400, 'msg': '密码和二级密码至少6位'})
|
||||
|
||||
from jituan.services.club_rbac import resolve_roles_for_binding
|
||||
|
||||
# 校验手机号是否已被后台客服占用(同俱乐部优先)
|
||||
existing_kefu = User.query.filter(
|
||||
Phone=phone, KefuProfile__isnull=False
|
||||
).order_by('-UserCreateTime').first()
|
||||
if existing_kefu:
|
||||
return Response({'code': 400, 'msg': '该账号已存在'})
|
||||
|
||||
if not isinstance(role_codes, list):
|
||||
return Response({'code': 400, 'msg': 'role_codes 格式错误'})
|
||||
if not role_codes:
|
||||
return Response({'code': 400, 'msg': '请至少选择一个角色'})
|
||||
|
||||
# 仅绑定前端所选角色(各俱乐部角色名不同,勿硬编码「客服」)
|
||||
role_names_to_bind = []
|
||||
seen_names = set()
|
||||
for name in role_codes:
|
||||
n = (name or '').strip()
|
||||
if n and n not in seen_names:
|
||||
seen_names.add(n)
|
||||
role_names_to_bind.append(n)
|
||||
|
||||
roles_to_bind, missing_roles = resolve_roles_for_binding(role_names_to_bind, target_club_id)
|
||||
if missing_roles:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'msg': f'角色不存在或与当前俱乐部不匹配:{", ".join(missing_roles)}',
|
||||
})
|
||||
if not roles_to_bind:
|
||||
return Response({'code': 400, 'msg': '请至少选择一个有效角色'})
|
||||
|
||||
with transaction.atomic():
|
||||
# 事务内二次校验,防止并发重复创建
|
||||
if User.query.filter(Phone=phone, KefuProfile__isnull=False).exists():
|
||||
return Response({'code': 400, 'msg': '该账号已存在'})
|
||||
|
||||
# 生成唯一的 yonghuid(7位数字)
|
||||
while True:
|
||||
yonghuid = str(random.randint(1000000, 9999999))
|
||||
if not User.query.filter(UserUID=yonghuid).exists():
|
||||
break
|
||||
|
||||
# 生成唯一的 openid(格式:admin_{时间戳}_{随机6位})
|
||||
timestamp = str(int(time.time() * 1000))
|
||||
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
openid = f"admin_{timestamp}_{random_suffix}"
|
||||
while User.query.filter(OpenID=openid).exists():
|
||||
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
openid = f"admin_{timestamp}_{random_suffix}"
|
||||
|
||||
user_main = User.query.create(
|
||||
UserUID=yonghuid,
|
||||
UserName=openid,
|
||||
OpenID=openid,
|
||||
Phone=phone,
|
||||
ClubID=target_club_id,
|
||||
)
|
||||
user_main.SetPassword(password)
|
||||
user_main.save(update_fields=['UserPassword', 'ClubID'])
|
||||
# 创建客服扩展表(后台用户扩展)
|
||||
import bcrypt as _bcrypt
|
||||
erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
|
||||
UserKefu.query.create(
|
||||
user=user_main,
|
||||
nicheng=nicheng,
|
||||
erjimima=erjimima_hashed,
|
||||
zhuangtai=1
|
||||
)
|
||||
for role in roles_to_bind:
|
||||
UserRole.objects.get_or_create(
|
||||
UserUUID=user_main.UserUUID,
|
||||
RoleUUID=role.RoleUUID,
|
||||
defaults={
|
||||
'UserRoleUUID': uuid.uuid4().bytes,
|
||||
'CompanyUUID': uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
|
||||
},
|
||||
)
|
||||
|
||||
from jituan.services.admin_assignments import create_or_reactivate_assignment
|
||||
from jituan.constants import DATA_SCOPE_SINGLE
|
||||
create_or_reactivate_assignment(
|
||||
yonghuid=yonghuid,
|
||||
club_id=target_club_id,
|
||||
role_code='CLUB_ADMIN',
|
||||
data_scope=DATA_SCOPE_SINGLE,
|
||||
is_primary=True,
|
||||
granted_by=request.user.UserUID,
|
||||
)
|
||||
|
||||
return Response({'code': 0, 'msg': '添加成功'})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
362
backend/views/sections.py
Normal file
362
backend/views/sections.py
Normal file
@@ -0,0 +1,362 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
class HqbkxxView(APIView):
|
||||
"""
|
||||
获取板块配置所需全部数据
|
||||
POST /houtai/hqbkxx
|
||||
Body: { "phone": "管理员手机号" }
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
# 1. 从前端获取传递的账号,用于防越权校验
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
|
||||
# 2. 调用公共验证方法
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
|
||||
# 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
|
||||
# ---------- 验证通过 ----------
|
||||
# 4. 检查是否有板块管理权限
|
||||
if 'bankuai' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
|
||||
scope_err = block_non_group_catalog_scope(request)
|
||||
if scope_err:
|
||||
return Response({'code': 400, 'msg': scope_err})
|
||||
|
||||
# 5. 查询所有板块
|
||||
bankuai_list = []
|
||||
for bk in Bankuai.query.all():
|
||||
bankuai_list.append({
|
||||
'bankuai_id': bk.bankuai_id,
|
||||
'mingcheng': bk.mingcheng,
|
||||
'CreateTime': bk.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if bk.CreateTime else ''
|
||||
})
|
||||
|
||||
# 6. 查询所有商品类型
|
||||
shangpin_leixing_list = []
|
||||
for sp in ShangpinLeixing.query.all():
|
||||
shangpin_leixing_list.append({
|
||||
'id': sp.id,
|
||||
'jieshao': sp.jieshao,
|
||||
'tupian_url': sp.tupian_url or '',
|
||||
'bankuai_id': sp.bankuai_id
|
||||
})
|
||||
|
||||
# 7. 查询会员(附带当前俱乐部售价)
|
||||
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
|
||||
from jituan.models import ClubHuiyuanPrice
|
||||
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
|
||||
|
||||
scope = resolve_club_scope(request)
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
price_map = {}
|
||||
if scope != DATA_SCOPE_ALL:
|
||||
for row in ClubHuiyuanPrice.query.filter(club_id=club_id):
|
||||
price_map[row.huiyuan_id] = row
|
||||
|
||||
huiyuan_list = []
|
||||
# 在受限 scope 下预过滤,避免 Huiyuan 全表遍历
|
||||
# 注:当 scope != DATA_SCOPE_ALL 且 club_id != CLUB_ID_DEFAULT 时,
|
||||
# 只有 price_map 中存在记录的会员才可能被保留(无记录的会被跳过)
|
||||
huiyuan_qs = Huiyuan.query.all()
|
||||
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT:
|
||||
huiyuan_qs = huiyuan_qs.filter(huiyuan_id__in=list(price_map.keys()))
|
||||
# 仅查询所需字段,减少内存占用(dict 比 model 实例更轻)
|
||||
huiyuan_qs = huiyuan_qs.values('huiyuan_id', 'jieshao', 'bankuai_id', 'jiage', 'guanshifc', 'zuzhangfc')
|
||||
for hy in huiyuan_qs:
|
||||
row = price_map.get(hy['huiyuan_id'])
|
||||
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT and row is None:
|
||||
continue
|
||||
if row is not None and not row.is_enabled:
|
||||
continue
|
||||
item = {
|
||||
'huiyuan_id': hy['huiyuan_id'],
|
||||
'jieshao': hy['jieshao'],
|
||||
'bankuai_id': hy['bankuai_id'],
|
||||
'jiage': str(row.jiage) if row else str(hy['jiage']),
|
||||
'guanshifc': str(row.guanshifc) if row else str(hy['guanshifc']),
|
||||
'zuzhangfc': str(row.zuzhangfc) if row else str(hy['zuzhangfc']),
|
||||
}
|
||||
huiyuan_list.append(item)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'bankuai': bankuai_list,
|
||||
'shangpin_leixing': shangpin_leixing_list,
|
||||
'huiyuan': huiyuan_list,
|
||||
'club_id': club_id,
|
||||
'scope': scope,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class BkxgView(APIView):
|
||||
"""
|
||||
板块增删改统一接口
|
||||
POST /houtai/bkxg
|
||||
Body: {
|
||||
"phone": "管理员手机号",
|
||||
"action": "create | update | delete | add_items | remove_items",
|
||||
"bankuai_id": int,
|
||||
"bankuai_name": str,
|
||||
"item_type": "shangpin_leixing | huiyuan",
|
||||
"item_ids": [1,2,3]
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
# 1. 从前端获取传递的账号
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
|
||||
# 2. 调用公共验证方法
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
|
||||
# 3. 验证失败直接返回
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
|
||||
# 4. 检查板块管理权限
|
||||
if 'bankuai' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
|
||||
scope_err = block_non_group_catalog_scope(request)
|
||||
if scope_err:
|
||||
return Response({'code': 400, 'msg': scope_err})
|
||||
|
||||
# 5. 获取操作类型
|
||||
action = request.data.get('action')
|
||||
if not action:
|
||||
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
|
||||
|
||||
# ==================== 创建板块 ====================
|
||||
if action == 'create':
|
||||
bankuai_name = request.data.get('bankuai_name', '').strip()
|
||||
if not bankuai_name:
|
||||
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
|
||||
if Bankuai.query.filter(mingcheng=bankuai_name).exists():
|
||||
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
|
||||
|
||||
shangpin_ids = request.data.get('shangpin_ids', [])
|
||||
huiyuan_ids = request.data.get('huiyuan_ids', [])
|
||||
|
||||
with transaction.atomic():
|
||||
new_bk = Bankuai.query.create(mingcheng=bankuai_name)
|
||||
if shangpin_ids:
|
||||
ShangpinLeixing.query.filter(
|
||||
id__in=shangpin_ids,
|
||||
bankuai__isnull=True
|
||||
).update(bankuai=new_bk)
|
||||
if huiyuan_ids:
|
||||
Huiyuan.query.filter(
|
||||
huiyuan_id__in=huiyuan_ids,
|
||||
bankuai__isnull=True
|
||||
).update(bankuai=new_bk)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '创建成功',
|
||||
'data': {'bankuai_id': new_bk.bankuai_id}
|
||||
})
|
||||
|
||||
# ==================== 以下操作需要 bankuai_id ====================
|
||||
bankuai_id = request.data.get('bankuai_id')
|
||||
if not bankuai_id:
|
||||
return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400)
|
||||
|
||||
try:
|
||||
bk = Bankuai.query.get(bankuai_id=bankuai_id)
|
||||
except Bankuai.DoesNotExist:
|
||||
return Response({'code': 1, 'msg': '板块不存在'}, status=404)
|
||||
|
||||
# ==================== 修改板块名称 ====================
|
||||
if action == 'update':
|
||||
new_name = request.data.get('bankuai_name', '').strip()
|
||||
if not new_name:
|
||||
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
|
||||
if Bankuai.query.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists():
|
||||
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
|
||||
bk.mingcheng = new_name
|
||||
bk.save()
|
||||
return Response({'code': 0, 'msg': '修改成功'})
|
||||
|
||||
# ==================== 删除板块 ====================
|
||||
elif action == 'delete':
|
||||
with transaction.atomic():
|
||||
ShangpinLeixing.query.filter(bankuai=bk).update(bankuai=None)
|
||||
Huiyuan.query.filter(bankuai=bk).update(bankuai=None)
|
||||
bk.delete()
|
||||
return Response({'code': 0, 'msg': '删除成功'})
|
||||
|
||||
# ==================== 添加绑定 ====================
|
||||
elif action == 'add_items':
|
||||
item_type = request.data.get('item_type')
|
||||
item_ids = request.data.get('item_ids', [])
|
||||
if not item_type or not item_ids:
|
||||
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
|
||||
|
||||
if item_type == 'shangpin_leixing':
|
||||
ShangpinLeixing.query.filter(
|
||||
id__in=item_ids,
|
||||
bankuai__isnull=True
|
||||
).update(bankuai=bk)
|
||||
elif item_type == 'huiyuan':
|
||||
Huiyuan.query.filter(
|
||||
huiyuan_id__in=item_ids,
|
||||
bankuai__isnull=True
|
||||
).update(bankuai=bk)
|
||||
else:
|
||||
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
|
||||
return Response({'code': 0, 'msg': '添加成功'})
|
||||
|
||||
# ==================== 移除绑定 ====================
|
||||
elif action == 'remove_items':
|
||||
item_type = request.data.get('item_type')
|
||||
item_ids = request.data.get('item_ids', [])
|
||||
if not item_type or not item_ids:
|
||||
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
|
||||
|
||||
if item_type == 'shangpin_leixing':
|
||||
ShangpinLeixing.query.filter(
|
||||
id__in=item_ids,
|
||||
bankuai=bk
|
||||
).update(bankuai=None)
|
||||
elif item_type == 'huiyuan':
|
||||
Huiyuan.query.filter(
|
||||
huiyuan_id__in=item_ids,
|
||||
bankuai=bk
|
||||
).update(bankuai=None)
|
||||
else:
|
||||
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
|
||||
return Response({'code': 0, 'msg': '移除成功'})
|
||||
|
||||
else:
|
||||
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
973
backend/views/shops.py
Normal file
973
backend/views/shops.py
Normal file
@@ -0,0 +1,973 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
has_perm_code,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
SHOP_MANAGE_PERMS = ('999a', '999b', '999c', '999d', '999e')
|
||||
|
||||
class ShopListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
# 1. 调用公共权限验证(与您给的示例完全一致)
|
||||
username_frontend = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username_frontend)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||
|
||||
# 2. 检查是否拥有所需权限(999a~999e 任意一个)
|
||||
if not has_perm_code(permissions, *SHOP_MANAGE_PERMS):
|
||||
return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'})
|
||||
|
||||
# 3. 解析请求参数
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 10))
|
||||
dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip()
|
||||
zhuangtai = request.data.get('zhuangtai')
|
||||
lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip()
|
||||
user_id = request.data.get('user_id', '').strip()
|
||||
ketixian_yue_min = request.data.get('ketixian_yue_min')
|
||||
ketixian_yue_max = request.data.get('ketixian_yue_max')
|
||||
zhengqu_zonge_min = request.data.get('zhengqu_zonge_min')
|
||||
zhengqu_zonge_max = request.data.get('zhengqu_zonge_max')
|
||||
|
||||
# 4. 构建查询集(先查店铺,预加载关联凭证)
|
||||
queryset = Dianpu.query.select_related('pingzheng').all()
|
||||
|
||||
if dianpu_mingcheng:
|
||||
queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng)
|
||||
if zhuangtai is not None:
|
||||
queryset = queryset.filter(zhuangtai=zhuangtai)
|
||||
if lianxi_dianhua:
|
||||
queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua)
|
||||
if user_id:
|
||||
queryset = queryset.filter(pingzheng__yonghu=user_id)
|
||||
|
||||
# 金额范围过滤
|
||||
if ketixian_yue_min is not None:
|
||||
queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min)
|
||||
if ketixian_yue_max is not None:
|
||||
queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max)
|
||||
if zhengqu_zonge_min is not None:
|
||||
queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min)
|
||||
if zhengqu_zonge_max is not None:
|
||||
queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max)
|
||||
|
||||
# 5. 分页
|
||||
paginator = Paginator(queryset, page_size)
|
||||
page_obj = paginator.get_page(page)
|
||||
|
||||
# 6. 构造返回数据
|
||||
shop_list = []
|
||||
for dianpu in page_obj:
|
||||
pingzheng = dianpu.pingzheng
|
||||
shop_list.append({
|
||||
'id': dianpu.id,
|
||||
'dianpu_mingcheng': dianpu.dianpu_mingcheng,
|
||||
'dianpu_touxiang': dianpu.dianpu_touxiang or '',
|
||||
'lianxi_dianhua': dianpu.lianxi_dianhua or '',
|
||||
'weixinhao': dianpu.weixinhao or '',
|
||||
'zhuangtai': dianpu.zhuangtai,
|
||||
'bangding_yonghushu': dianpu.bangding_yonghushu,
|
||||
'erweima_url': dianpu.erweima_url or '',
|
||||
'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng,
|
||||
'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0,
|
||||
'ketixian_yue': dianpu.ketixian_yue,
|
||||
'zhengqu_zonge': dianpu.zhengqu_zonge,
|
||||
'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0,
|
||||
'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane,
|
||||
'pingzheng__yonghu': pingzheng.yonghu, # 用户ID
|
||||
'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号
|
||||
})
|
||||
|
||||
# 获取默认配置(ID=1,不存在则创建)
|
||||
default_config, _ = DianpuMorenPeizhi.query.get_or_create(id=1, defaults={
|
||||
'moren_choucheng_feilv': 0.1,
|
||||
'moren_tixian_xiane': 1000.00
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': {
|
||||
'list': shop_list,
|
||||
'total': paginator.count,
|
||||
'default_config': {
|
||||
'moren_choucheng_feilv': default_config.moren_choucheng_feilv,
|
||||
'moren_tixian_xiane': default_config.moren_tixian_xiane,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class ShopModifyView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
# 1. 身份验证
|
||||
username_frontend = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username_frontend)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||
|
||||
action = request.data.get('action', '')
|
||||
|
||||
# 2. 根据 action 分派任务
|
||||
if action == 'update_dianpu':
|
||||
return self._update_dianpu(request, permissions)
|
||||
elif action == 'add_dianpu':
|
||||
return self._add_dianpu(request, permissions)
|
||||
elif action == 'update_default':
|
||||
return self._update_default(request, permissions)
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '未知操作类型'})
|
||||
|
||||
def _update_dianpu(self, request, permissions):
|
||||
"""修改店铺信息,精确权限控制"""
|
||||
dianpu_id = request.data.get('dianpu_id')
|
||||
if not dianpu_id:
|
||||
return Response({'code': 400, 'msg': '缺少店铺ID'})
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id)
|
||||
|
||||
# 基础信息修改 (999e)
|
||||
if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']):
|
||||
if not has_perm_code(permissions, '999e'):
|
||||
return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'})
|
||||
if 'dianpu_mingcheng' in request.data:
|
||||
dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng']
|
||||
if 'lianxi_dianhua' in request.data:
|
||||
dianpu.lianxi_dianhua = request.data['lianxi_dianhua']
|
||||
if 'weixinhao' in request.data:
|
||||
dianpu.weixinhao = request.data['weixinhao']
|
||||
|
||||
# 店铺状态 (999a)
|
||||
if 'zhuangtai' in request.data:
|
||||
if not has_perm_code(permissions, '999a'):
|
||||
return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'})
|
||||
new_status = int(request.data['zhuangtai'])
|
||||
if new_status not in (0, 1):
|
||||
return Response({'code': 400, 'msg': '店铺状态无效'})
|
||||
dianpu.zhuangtai = new_status
|
||||
|
||||
# 可提现余额 (999b)
|
||||
if 'ketixian_yue' in request.data:
|
||||
if not has_perm_code(permissions, '999b'):
|
||||
return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'})
|
||||
dianpu.ketixian_yue = request.data['ketixian_yue']
|
||||
|
||||
# 提现限额相关 (999c)
|
||||
if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data:
|
||||
if not has_perm_code(permissions, '999c'):
|
||||
return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'})
|
||||
if 'kaiqi_meiri_xiane' in request.data:
|
||||
dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane']
|
||||
if 'meiri_tixian_xiane' in request.data:
|
||||
dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane']
|
||||
|
||||
# 抽成相关 (999d)
|
||||
if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data:
|
||||
if not has_perm_code(permissions, '999d'):
|
||||
return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'})
|
||||
if 'kaiqi_shouyi_choucheng' in request.data:
|
||||
dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng']
|
||||
if 'shouyi_choucheng_feilv' in request.data:
|
||||
dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv']
|
||||
|
||||
dianpu.save() # Django 会自动判断更新哪些字段
|
||||
return Response({'code': 0, 'msg': '店铺信息更新成功'})
|
||||
|
||||
except Dianpu.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '店铺不存在'})
|
||||
except Exception as e:
|
||||
logger.exception("修改店铺失败")
|
||||
return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
|
||||
|
||||
def _add_dianpu(self, request, permissions):
|
||||
"""添加店铺,需要权限 999b"""
|
||||
if not has_perm_code(permissions, '999b'):
|
||||
return Response({'code': 403, 'msg': '无权限添加店铺(999b)'})
|
||||
|
||||
yonghu_id = request.data.get('yonghu_id')
|
||||
zhanghao = request.data.get('zhanghao')
|
||||
mima = request.data.get('mima')
|
||||
dianpu_mingcheng = request.data.get('dianpu_mingcheng')
|
||||
|
||||
if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]):
|
||||
return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'})
|
||||
|
||||
# 检查用户是否存在
|
||||
if not User.query.filter(UserUID=yonghu_id).exists():
|
||||
return Response({'code': 404, 'msg': '用户不存在'})
|
||||
|
||||
# 检查凭证是否已存在
|
||||
if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists():
|
||||
return Response({'code': 400, 'msg': '登录账号已存在'})
|
||||
if YonghuPingzheng.query.filter(yonghu=yonghu_id).exists():
|
||||
return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'})
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
pingzheng = YonghuPingzheng.query.create(
|
||||
yonghu=yonghu_id,
|
||||
zhanghao=zhanghao,
|
||||
mima=mima,
|
||||
is_active=True
|
||||
)
|
||||
dianpu = Dianpu.query.create(
|
||||
pingzheng=pingzheng,
|
||||
dianpu_mingcheng=dianpu_mingcheng,
|
||||
lianxi_dianhua=request.data.get('lianxi_dianhua', ''),
|
||||
weixinhao=request.data.get('weixinhao', ''),
|
||||
kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False),
|
||||
shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0),
|
||||
kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False),
|
||||
meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0),
|
||||
)
|
||||
return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id})
|
||||
except IntegrityError as e:
|
||||
logger.error("创建店铺违反唯一性约束: %s", e)
|
||||
return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'})
|
||||
except Exception as e:
|
||||
logger.exception("创建店铺失败")
|
||||
return Response({'code': 500, 'msg': '创建失败,请稍后重试'})
|
||||
|
||||
def _update_default(self, request, permissions):
|
||||
"""修改默认配置,按修改内容校验权限"""
|
||||
feilv = request.data.get('moren_choucheng_feilv')
|
||||
xiane = request.data.get('moren_tixian_xiane')
|
||||
modify_commission = feilv is not None
|
||||
modify_withdraw = xiane is not None
|
||||
|
||||
if modify_commission and not has_perm_code(permissions, '999d'):
|
||||
return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'})
|
||||
if modify_withdraw and not has_perm_code(permissions, '999c'):
|
||||
return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'})
|
||||
|
||||
try:
|
||||
config, _ = DianpuMorenPeizhi.query.get_or_create(id=1)
|
||||
if modify_commission:
|
||||
config.moren_choucheng_feilv = feilv
|
||||
if modify_withdraw:
|
||||
config.moren_tixian_xiane = xiane
|
||||
config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) +
|
||||
(['moren_tixian_xiane'] if modify_withdraw else []))
|
||||
return Response({'code': 0, 'msg': '默认配置更新成功'})
|
||||
except Exception as e:
|
||||
logger.exception("更新默认配置失败")
|
||||
return Response({'code': 500, 'msg': '更新失败'})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ==================== 1. 公共商品类型 + 审核模式 ====================
|
||||
class ShopPublicTypeAndAuditView(APIView):
|
||||
"""
|
||||
返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关
|
||||
前端请求路径:/houtai/htdphqsplx
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
# ---------- 身份与权限校验 ----------
|
||||
username = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||
|
||||
# 拥有 1199ab/1199abc/1199abd 任一个即可访问
|
||||
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
|
||||
return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'})
|
||||
|
||||
# ---------- 查询公共商品类型(仅正常审核状态的) ----------
|
||||
try:
|
||||
# shenhezhuangtai = 1 表示正常 (根据模型默认值)
|
||||
types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values(
|
||||
'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin'
|
||||
)
|
||||
type_list = []
|
||||
for t in types_qs:
|
||||
type_list.append({
|
||||
'id': t['id'],
|
||||
'jieshao': t['jieshao'] or '',
|
||||
'tupian_url': t['tupian_url'] or '',
|
||||
'yaoqiuleixing': t['yaoqiuleixing'],
|
||||
'huiyuan_id': t['huiyuan_id'],
|
||||
'yongjin': t['yongjin'],
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("查询公共商品类型失败")
|
||||
return Response({'code': 500, 'msg': '服务器错误,请稍后重试'})
|
||||
|
||||
# ---------- 获取审核模式配置 ----------
|
||||
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
|
||||
kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'public_types': type_list,
|
||||
'kaiqi_shenhe': kaiqi_shenhe
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
# ==================== 2. 店铺列表(支持搜索、筛选、分页) ====================
|
||||
class ShopListForProductView(APIView):
|
||||
"""
|
||||
返回店铺列表,用于商品管理页面选择店铺。
|
||||
支持:关键词搜索(店铺ID/名称/用户ID)、店铺状态筛选、分页。
|
||||
前端请求路径:/houtai/htdphqdpys
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败'})
|
||||
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
|
||||
return Response({'code': 403, 'msg': '无权访问店铺列表'})
|
||||
|
||||
# ---------- 安全解析分页参数 ----------
|
||||
try:
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 10))
|
||||
except (TypeError, ValueError):
|
||||
return Response({'code': 400, 'msg': '分页参数格式错误'})
|
||||
|
||||
# 搜索关键词(字符串,ORM 自动防注入)
|
||||
keyword = str(request.data.get('keyword', '')).strip()
|
||||
# 店铺状态筛选(1正常,0封禁,不传则全部)
|
||||
zhuangtai = request.data.get('zhuangtai')
|
||||
|
||||
# ---------- 构建查询集(关联凭证表获取用户ID) ----------
|
||||
shops = Dianpu.query.select_related('pingzheng').all()
|
||||
|
||||
# 状态筛选:只接受 0 或 1
|
||||
if zhuangtai is not None:
|
||||
try:
|
||||
zhuangtai = int(zhuangtai)
|
||||
if zhuangtai in [0, 1]:
|
||||
shops = shops.filter(zhuangtai=zhuangtai)
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '状态参数只能为0或1'})
|
||||
except (TypeError, ValueError):
|
||||
return Response({'code': 400, 'msg': '状态参数必须为整数'})
|
||||
|
||||
# 关键词搜索(店铺ID/名称/用户ID)
|
||||
if keyword:
|
||||
shops = shops.filter(
|
||||
models.Q(id__icontains=keyword) |
|
||||
models.Q(dianpu_mingcheng__icontains=keyword) |
|
||||
models.Q(pingzheng__yonghu__icontains=keyword)
|
||||
)
|
||||
|
||||
# ---------- 分页 ----------
|
||||
paginator = Paginator(shops, page_size)
|
||||
page_obj = paginator.get_page(page)
|
||||
|
||||
shop_list = []
|
||||
for shop in page_obj:
|
||||
shop_list.append({
|
||||
'id': shop.id,
|
||||
'dianpu_mingcheng': shop.dianpu_mingcheng,
|
||||
'zhuangtai': shop.zhuangtai,
|
||||
'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '',
|
||||
'lianxi_dianhua': shop.lianxi_dianhua or '',
|
||||
'weixinhao': shop.weixinhao or '',
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'list': shop_list,
|
||||
'total': paginator.count
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ====================
|
||||
class ShopProductTypeMappingView(APIView):
|
||||
"""
|
||||
返回店铺商品类型映射(ShangpinLeixingDianpu)列表。
|
||||
支持:店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。
|
||||
每次请求同时返回“待审核总数”(shenhe_zhuangtai=False 的记录数)。
|
||||
前端请求路径:/houtai/htdphqyssplx
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败'})
|
||||
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
|
||||
return Response({'code': 403, 'msg': '无权访问商品类型映射'})
|
||||
|
||||
# ---------- 安全解析参数 ----------
|
||||
try:
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 10))
|
||||
except (TypeError, ValueError):
|
||||
return Response({'code': 400, 'msg': '分页参数格式错误'})
|
||||
|
||||
keyword = str(request.data.get('keyword', '')).strip()
|
||||
shop_id = request.data.get('shop_id')
|
||||
public_type_id = request.data.get('public_type_id')
|
||||
|
||||
# 布尔型筛选字段处理:None 表示不筛选,否则转为布尔值
|
||||
def parse_bool(val):
|
||||
if val is None:
|
||||
return None
|
||||
return bool(val)
|
||||
|
||||
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
|
||||
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
|
||||
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
|
||||
|
||||
# ---------- 查询(连表店铺和公共类型) ----------
|
||||
mappings = ShangpinLeixingDianpu.query.select_related('dianpu', 'gonggong_leixing').all()
|
||||
|
||||
# 筛选条件:均为参数化,完全防注入
|
||||
if shop_id is not None:
|
||||
mappings = mappings.filter(dianpu_id=int(shop_id))
|
||||
if public_type_id is not None:
|
||||
mappings = mappings.filter(gonggong_leixing_id=int(public_type_id))
|
||||
if shangjia is not None:
|
||||
mappings = mappings.filter(shangjia_zhuangtai=shangjia)
|
||||
if fengjin is not None:
|
||||
mappings = mappings.filter(fengjin_zhuangtai=fengjin)
|
||||
if shenhe is not None:
|
||||
mappings = mappings.filter(shenhe_zhuangtai=shenhe)
|
||||
|
||||
# 关键字搜索(介绍、ID、店铺名、公共类型名)
|
||||
if keyword:
|
||||
mappings = mappings.filter(
|
||||
models.Q(jieshao__icontains=keyword) |
|
||||
models.Q(id__icontains=keyword) |
|
||||
models.Q(dianpu__dianpu_mingcheng__icontains=keyword) |
|
||||
models.Q(gonggong_leixing__jieshao__icontains=keyword)
|
||||
)
|
||||
|
||||
# ---------- 待审核总数(不受分页影响) ----------
|
||||
pending_total = mappings.filter(shenhe_zhuangtai=False).count()
|
||||
|
||||
# ---------- 分页 ----------
|
||||
paginator = Paginator(mappings, page_size)
|
||||
page_obj = paginator.get_page(page)
|
||||
|
||||
mapping_list = []
|
||||
for m in page_obj:
|
||||
mapping_list.append({
|
||||
'id': m.id,
|
||||
'dianpu_id': m.dianpu_id,
|
||||
'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '',
|
||||
'gonggong_leixing_id': m.gonggong_leixing_id,
|
||||
'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '',
|
||||
'jieshao': m.jieshao,
|
||||
'tupian_url': m.tupian_url or '',
|
||||
'paixu': m.paixu,
|
||||
'shangjia_zhuangtai': m.shangjia_zhuangtai,
|
||||
'fengjin_zhuangtai': m.fengjin_zhuangtai,
|
||||
'shenhe_zhuangtai': m.shenhe_zhuangtai,
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'list': mapping_list,
|
||||
'total': paginator.count,
|
||||
'pending_total': pending_total
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
# ==================== 统一修改接口(已修正事务) ====================
|
||||
class ShopProductModifyView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败'})
|
||||
|
||||
user_perms = permissions
|
||||
action = request.data.get('action')
|
||||
|
||||
if action == 'update_audit_mode':
|
||||
return self._update_audit_mode(request, user_perms)
|
||||
elif action == 'update_mapping_status':
|
||||
return self._update_mapping_status(request, user_perms)
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '未知操作'})
|
||||
|
||||
@transaction.atomic # ✅ 事务修复点
|
||||
def _update_audit_mode(self, request, user_perms):
|
||||
if not has_perm_code(permissions, '1199ab'):
|
||||
return Response({'code': 403, 'msg': '无权限修改审核模式'})
|
||||
|
||||
kaiqi = request.data.get('kaiqi_shenhe')
|
||||
if kaiqi is None:
|
||||
return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'})
|
||||
|
||||
try:
|
||||
config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1)
|
||||
config.kaiqi_shenhe = bool(kaiqi)
|
||||
config.save(update_fields=['kaiqi_shenhe'])
|
||||
return Response({'code': 0, 'msg': '审核模式已更新'})
|
||||
except Exception as e:
|
||||
logger.exception("更新审核模式失败")
|
||||
return Response({'code': 500, 'msg': '服务器错误'})
|
||||
|
||||
@transaction.atomic
|
||||
def _update_mapping_status(self, request, user_perms):
|
||||
mapping_id = request.data.get('mapping_id')
|
||||
if not mapping_id:
|
||||
return Response({'code': 400, 'msg': '缺少 mapping_id'})
|
||||
|
||||
try:
|
||||
mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id))
|
||||
except (ShangpinLeixingDianpu.DoesNotExist, ValueError):
|
||||
return Response({'code': 404, 'msg': '映射不存在'})
|
||||
|
||||
fengjin = request.data.get('fengjin_zhuangtai')
|
||||
shangjia = request.data.get('shangjia_zhuangtai')
|
||||
shenhe = request.data.get('shenhe_zhuangtai')
|
||||
|
||||
# 权限判断
|
||||
if fengjin is not None:
|
||||
if fengjin:
|
||||
if not has_perm_code(user_perms, '1199abc'):
|
||||
return Response({'code': 403, 'msg': '无权限封禁'})
|
||||
else:
|
||||
if not has_perm_code(user_perms, '1199abd'):
|
||||
return Response({'code': 403, 'msg': '无权限解封'})
|
||||
mapping.fengjin_zhuangtai = bool(fengjin)
|
||||
|
||||
if shangjia is not None:
|
||||
if not shangjia:
|
||||
if not has_perm_code(user_perms, '1199abc'):
|
||||
return Response({'code': 403, 'msg': '无权限下架'})
|
||||
else:
|
||||
if not has_perm_code(user_perms, '1199abd'):
|
||||
return Response({'code': 403, 'msg': '无权限上架'})
|
||||
mapping.shangjia_zhuangtai = bool(shangjia)
|
||||
|
||||
if shenhe is not None:
|
||||
if shenhe:
|
||||
if not has_perm_code(user_perms, '1199abd'):
|
||||
return Response({'code': 403, 'msg': '无权限审核通过'})
|
||||
mapping.shenhe_zhuangtai = bool(shenhe)
|
||||
|
||||
mapping.save()
|
||||
return Response({'code': 0, 'msg': '修改成功'})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 商品管理相关权限组(拥有任意一个即可查看列表)
|
||||
PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe']
|
||||
|
||||
|
||||
class ProductListView(APIView):
|
||||
"""
|
||||
商品列表接口,支持多条件筛选与分页。
|
||||
请求路径: /houtai/hqhtdpsplx
|
||||
权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
# ----- 身份与权限验证 -----
|
||||
username = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败'})
|
||||
|
||||
if not has_perm_code(permissions, *PRODUCT_PERMS):
|
||||
return Response({'code': 403, 'msg': '无商品管理权限'})
|
||||
|
||||
# ----- 安全解析分页与筛选参数 -----
|
||||
try:
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 15))
|
||||
except (TypeError, ValueError):
|
||||
return Response({'code': 400, 'msg': '分页参数格式错误'})
|
||||
|
||||
keyword = str(request.data.get('keyword', '')).strip()
|
||||
shop_id = request.data.get('shop_id') # 店铺ID (可选)
|
||||
shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选)
|
||||
public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选)
|
||||
|
||||
# 布尔型状态筛选
|
||||
def parse_bool(val):
|
||||
if val is None:
|
||||
return None
|
||||
return bool(val)
|
||||
|
||||
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
|
||||
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
|
||||
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
|
||||
|
||||
# ----- 构建查询(预加载店铺和店铺商品类型) -----
|
||||
products = Shangpin.query.select_related('dianpu', 'dianpu_leixing').all()
|
||||
|
||||
# 按店铺筛选
|
||||
if shop_id is not None:
|
||||
try:
|
||||
products = products.filter(dianpu_id=int(shop_id))
|
||||
except (ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': 'shop_id 格式错误'})
|
||||
|
||||
# 按店铺商品类型映射筛选
|
||||
if shop_product_type_id is not None:
|
||||
try:
|
||||
products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
|
||||
except (ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
|
||||
|
||||
# 按公共商品类型筛选(leixing_id 存储的是公共类型的ID)
|
||||
if public_type_id is not None:
|
||||
try:
|
||||
products = products.filter(leixing_id=int(public_type_id))
|
||||
except (ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': 'public_type_id 格式错误'})
|
||||
|
||||
# 平台审核状态筛选 (BooleanField)
|
||||
if shenhe is not None:
|
||||
products = products.filter(shenhe_zhuangtai=shenhe)
|
||||
|
||||
# 上架状态筛选
|
||||
if shangjia is not None:
|
||||
products = products.filter(shangjia_zhuangtai=shangjia)
|
||||
|
||||
# 封禁状态筛选
|
||||
if fengjin is not None:
|
||||
products = products.filter(fengjin_zhuangtai=fengjin)
|
||||
|
||||
# 关键词搜索:商品ID或商品标题(搜索具有模糊性,使用icontains)
|
||||
if keyword:
|
||||
# 尝试纯数字可能是商品ID,使用精确匹配与标题模糊搜索
|
||||
products = products.filter(
|
||||
models.Q(id__icontains=keyword) |
|
||||
models.Q(biaoqian__icontains=keyword)
|
||||
)
|
||||
|
||||
# ----- 获取待审核总数(平台审核未通过的数量) -----
|
||||
pending_total = products.filter(shenhe_zhuangtai=False).count()
|
||||
|
||||
# ----- 分页 -----
|
||||
paginator = Paginator(products, page_size)
|
||||
page_obj = paginator.get_page(page)
|
||||
|
||||
# ----- 构造返回数据(批量获取公共类型名称,避免N+1) -----
|
||||
# 提取所有公共商品类型ID
|
||||
type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None}
|
||||
public_type_map = {}
|
||||
if type_ids:
|
||||
public_types = ShangpinLeixing.query.filter(id__in=type_ids).values('id', 'jieshao')
|
||||
public_type_map = {t['id']: t['jieshao'] for t in public_types}
|
||||
|
||||
product_list = []
|
||||
for p in page_obj:
|
||||
product_list.append({
|
||||
'id': p.id,
|
||||
'biaoqian': p.biaoqian or '',
|
||||
'jiage': p.jiage,
|
||||
'kucun': p.kucun,
|
||||
'leixing_id': p.leixing_id,
|
||||
'public_type_name': public_type_map.get(p.leixing_id, ''),
|
||||
'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
|
||||
'duiwai_xiaoliang': p.duiwai_xiaoliang,
|
||||
'jieshao': p.jieshao or '',
|
||||
'xiadan_xuzhi': p.xiadan_xuzhi or '',
|
||||
'guize_tupian': p.guize_tupian or '',
|
||||
'yaoqiuleixing': p.yaoqiuleixing,
|
||||
'huiyuan_id': p.huiyuan_id or '',
|
||||
'yongjin': p.yongjin,
|
||||
'kaioi_ewai_PlayerCommission': p.kaioi_ewai_dashou_fencheng,
|
||||
'ewai_PlayerCommission': p.ewai_dashou_fencheng,
|
||||
'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '',
|
||||
'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '',
|
||||
'paixu': p.paixu,
|
||||
'dianpu_id': p.dianpu_id,
|
||||
'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '',
|
||||
'dianpu_leixing_id': p.dianpu_leixing_id,
|
||||
'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '',
|
||||
'shangjia_zhuangtai': p.shangjia_zhuangtai,
|
||||
'fengjin_zhuangtai': p.fengjin_zhuangtai,
|
||||
'shenhe_zhuangtai': p.shenhe_zhuangtai,
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'list': product_list,
|
||||
'total': paginator.count,
|
||||
'pending_total': pending_total, # 待审核商品总数
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class ProductModifyView(APIView):
|
||||
"""
|
||||
商品修改接口,根据修改内容细粒度鉴权。
|
||||
请求路径: /houtai/htxgdpspsj
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败'})
|
||||
|
||||
action = request.data.get('action', '')
|
||||
|
||||
if action != 'update_product':
|
||||
return Response({'code': 400, 'msg': '未知操作'})
|
||||
|
||||
return self._update_product(request, permissions)
|
||||
|
||||
@transaction.atomic
|
||||
def _update_product(self, request, permissions):
|
||||
"""执行商品修改,严格校验权限"""
|
||||
product_id = request.data.get('product_id')
|
||||
if not product_id:
|
||||
return Response({'code': 400, 'msg': '缺少商品ID'})
|
||||
|
||||
try:
|
||||
# 行级锁防止并发修改
|
||||
product = Shangpin.objects.select_for_update().get(id=int(product_id))
|
||||
except (Shangpin.DoesNotExist, ValueError):
|
||||
return Response({'code': 404, 'msg': '商品不存在'})
|
||||
|
||||
# 需要修改的字段
|
||||
fields_to_update = {}
|
||||
# 将要修改的状态标志用于后续权限判断
|
||||
status_changes = {
|
||||
'shangjia': request.data.get('shangjia_zhuangtai'),
|
||||
'fengjin': request.data.get('fengjin_zhuangtai'),
|
||||
'shenhe': request.data.get('shenhe_zhuangtai'),
|
||||
}
|
||||
|
||||
# ---------- 权限分类检查 ----------
|
||||
# 1. 封禁/下架操作 (消极操作) -> 1199abc
|
||||
if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']):
|
||||
if not has_perm_code(permissions, '1199abc'):
|
||||
return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'})
|
||||
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True:
|
||||
if not has_perm_code(permissions, '1199abc'):
|
||||
return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'})
|
||||
|
||||
# 2. 解封/上架/过审 (积极操作) -> 1199abd
|
||||
if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']):
|
||||
if not has_perm_code(permissions, '1199abd'):
|
||||
return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'})
|
||||
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False:
|
||||
if not has_perm_code(permissions, '1199abd'):
|
||||
return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'})
|
||||
|
||||
# 3. 修改其他数据(非状态字段)-> 1199abe
|
||||
# 除了上架、封禁、审核之外的字段都属于“其他数据”
|
||||
other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username',
|
||||
'shangjia_zhuangtai', 'fengjin_zhuangtai',
|
||||
'shenhe_zhuangtai']]
|
||||
if other_fields and not has_perm_code(permissions, '1199abe'):
|
||||
return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'})
|
||||
|
||||
# ---------- 应用修改 ----------
|
||||
# 允许修改的字段白名单(防止注入)
|
||||
# 前端参数名 → 模型字段名映射
|
||||
field_name_map = {
|
||||
'kaioi_ewai_PlayerCommission': 'kaioi_ewai_dashou_fencheng',
|
||||
'ewai_PlayerCommission': 'ewai_dashou_fencheng',
|
||||
}
|
||||
allowed_fields = [
|
||||
'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang',
|
||||
'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin',
|
||||
'kaioi_ewai_PlayerCommission', 'ewai_PlayerCommission',
|
||||
'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai'
|
||||
]
|
||||
|
||||
update_fields = []
|
||||
for field in allowed_fields:
|
||||
value = request.data.get(field)
|
||||
if value is not None:
|
||||
model_field = field_name_map.get(field, field)
|
||||
# 简单类型校验(根据字段类型)
|
||||
if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_PlayerCommission', 'paixu']:
|
||||
try:
|
||||
value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value)
|
||||
except (ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'})
|
||||
setattr(product, model_field, value)
|
||||
update_fields.append(model_field)
|
||||
|
||||
if update_fields:
|
||||
product.save(update_fields=update_fields)
|
||||
return Response({'code': 0, 'msg': '商品修改成功'})
|
||||
|
||||
|
||||
class ShopCopyCatalogView(APIView):
|
||||
"""
|
||||
复制店铺商品目录(类型 + 专区 + 商品,含 OSS 图片独立拷贝)。
|
||||
请求路径: /houtai/htfzdpspsj
|
||||
权限: 店铺管理权限 999a~999e 任一
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||
|
||||
if not has_perm_code(permissions, *SHOP_MANAGE_PERMS):
|
||||
return Response({'code': 403, 'msg': '您没有权限执行店铺商品复制操作'})
|
||||
|
||||
source_user_uid = str(request.data.get('source_user_uid', '')).strip()
|
||||
target_user_uid = str(request.data.get('target_user_uid', '')).strip()
|
||||
copy_mode = str(request.data.get('copy_mode', 'append')).strip().lower()
|
||||
|
||||
if not source_user_uid or not target_user_uid:
|
||||
return Response({'code': 400, 'msg': '请填写源店铺与目标店铺的用户ID'})
|
||||
|
||||
from backend.services.shop_copy_service import copy_shop_catalog, ShopCopyError
|
||||
|
||||
try:
|
||||
result = copy_shop_catalog(source_user_uid, target_user_uid, copy_mode)
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': (
|
||||
f'复制成功:商品类型 {result["copied_types"]} 个,'
|
||||
f'专区 {result["copied_zones"]} 个,商品 {result["copied_products"]} 个'
|
||||
),
|
||||
'data': result,
|
||||
})
|
||||
except ShopCopyError as e:
|
||||
return Response({'code': e.code, 'msg': e.message})
|
||||
except Exception:
|
||||
logger.exception('复制店铺商品失败')
|
||||
return Response({'code': 500, 'msg': '复制失败,请稍后重试'})
|
||||
1166
backend/views/system.py
Normal file
1166
backend/views/system.py
Normal file
File diff suppressed because it is too large
Load Diff
350
backend/views/titles.py
Normal file
350
backend/views/titles.py
Normal file
@@ -0,0 +1,350 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
class KhpzhqView(APIView):
|
||||
"""
|
||||
获取所有考核配置数据(称号列表、板块、费用)
|
||||
POST /houtai/khpzhq
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'kaohepeizhi' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
|
||||
|
||||
bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
|
||||
|
||||
chenghao_qs = Chenghao.query.prefetch_related(
|
||||
Prefetch('kaohecishufeiyong_set', queryset=KaoheCishuFeiyong.objects.order_by('cishu'))
|
||||
).all()
|
||||
chenghao_list = []
|
||||
for ch in chenghao_qs:
|
||||
# 获取费用:按次数排序,转为 {cishu: feiyong} 字典
|
||||
# prefetch_related 已用 Prefetch 排序,直接遍历不会绕过缓存
|
||||
feiyong_dict = {
|
||||
item.cishu: float(item.feiyong)
|
||||
for item in ch.kaohecishufeiyong_set.all()
|
||||
}
|
||||
max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0
|
||||
|
||||
chenghao_list.append({
|
||||
'id': ch.id,
|
||||
'mingcheng': ch.mingcheng,
|
||||
'leixing': ch.leixing,
|
||||
'leixing_name': ch.get_leixing_display(),
|
||||
'bankuai_id': ch.bankuai_id,
|
||||
'tubiao_url': ch.tubiao_url or '',
|
||||
'texiao_miaoshu': ch.texiao_miaoshu or '',
|
||||
'CreateTime': ch.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if ch.CreateTime else '',
|
||||
'kaohe_guize': ch.kaohe_guize or '',
|
||||
'kaioi_jinpai': ch.kaioi_jinpai,
|
||||
'feiyong': feiyong_dict, # {1:100.0, 2:200.0}
|
||||
'max_cishu': max_cishu
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'bankuai': bankuai_list,
|
||||
'chenghao': chenghao_list
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("KhpzhqView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
class ChzsgcView(APIView):
|
||||
"""
|
||||
称号(标签)增删改查统一接口
|
||||
POST /houtai/chzsgc
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'kaohepeizhi' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
|
||||
|
||||
action = request.data.get('action')
|
||||
if not action:
|
||||
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
|
||||
|
||||
# ========== 创建称号 ==========
|
||||
if action == 'create':
|
||||
mingcheng = request.data.get('mingcheng', '').strip()
|
||||
leixing = request.data.get('leixing', 'dashou')
|
||||
bankuai_id = request.data.get('bankuai_id')
|
||||
texiao_miaoshu = request.data.get('texiao_miaoshu', '')
|
||||
kaohe_guize = request.data.get('kaohe_guize', '')
|
||||
kaioi_jinpai = request.data.get('kaioi_jinpai', False)
|
||||
feiyong_list = request.data.get('feiyong_list', [])
|
||||
|
||||
if not mingcheng:
|
||||
return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
|
||||
if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
|
||||
return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
|
||||
if Chenghao.query.filter(mingcheng=mingcheng).exists():
|
||||
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
|
||||
|
||||
with transaction.atomic():
|
||||
ch = Chenghao.query.create(
|
||||
mingcheng=mingcheng,
|
||||
leixing=leixing,
|
||||
bankuai_id=bankuai_id,
|
||||
texiao_miaoshu=texiao_miaoshu,
|
||||
kaohe_guize=kaohe_guize,
|
||||
kaioi_jinpai=kaioi_jinpai
|
||||
)
|
||||
# 处理费用:前端传来 [{cishu:1, feiyong:100}, ...]
|
||||
if feiyong_list:
|
||||
for item in feiyong_list:
|
||||
cishu = int(item.get('cishu', 1))
|
||||
feiyong = float(item.get('feiyong', 0))
|
||||
KaoheCishuFeiyong.query.update_or_create(
|
||||
chenghao=ch, cishu=cishu,
|
||||
defaults={'feiyong': feiyong}
|
||||
)
|
||||
else:
|
||||
# 默认添加第一次免费
|
||||
KaoheCishuFeiyong.query.create(chenghao=ch, cishu=1, feiyong=0)
|
||||
|
||||
return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}})
|
||||
|
||||
# ========== 修改称号 ==========
|
||||
elif action == 'update':
|
||||
ch_id = request.data.get('chenghao_id')
|
||||
if not ch_id:
|
||||
return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
|
||||
try:
|
||||
ch = Chenghao.query.get(id=ch_id)
|
||||
except Chenghao.DoesNotExist:
|
||||
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||||
|
||||
mingcheng = request.data.get('mingcheng', ch.mingcheng).strip()
|
||||
leixing = request.data.get('leixing', ch.leixing)
|
||||
bankuai_id = request.data.get('bankuai_id', ch.bankuai_id)
|
||||
texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu)
|
||||
kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize)
|
||||
kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai)
|
||||
|
||||
if not mingcheng:
|
||||
return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
|
||||
if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
|
||||
return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
|
||||
if Chenghao.query.filter(mingcheng=mingcheng).exclude(id=ch_id).exists():
|
||||
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
|
||||
|
||||
ch.mingcheng = mingcheng
|
||||
ch.leixing = leixing
|
||||
ch.bankuai_id = bankuai_id
|
||||
ch.texiao_miaoshu = texiao_miaoshu
|
||||
ch.kaohe_guize = kaohe_guize
|
||||
ch.kaioi_jinpai = kaioi_jinpai
|
||||
ch.save()
|
||||
|
||||
# 如果前端传递了 feiyong_list,则全量替换费用
|
||||
feiyong_list = request.data.get('feiyong_list')
|
||||
if feiyong_list is not None:
|
||||
with transaction.atomic():
|
||||
ch.kaohecishufeiyong_set.all().delete()
|
||||
for item in feiyong_list:
|
||||
KaoheCishuFeiyong.query.create(
|
||||
chenghao=ch,
|
||||
cishu=int(item.get('cishu', 1)),
|
||||
feiyong=float(item.get('feiyong', 0))
|
||||
)
|
||||
|
||||
return Response({'code': 0, 'msg': '修改成功'})
|
||||
|
||||
# ========== 删除称号 ==========
|
||||
elif action == 'delete':
|
||||
ch_id = request.data.get('chenghao_id')
|
||||
if not ch_id:
|
||||
return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
|
||||
try:
|
||||
ch = Chenghao.query.get(id=ch_id)
|
||||
except Chenghao.DoesNotExist:
|
||||
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||||
ch.delete()
|
||||
return Response({'code': 0, 'msg': '删除成功'})
|
||||
|
||||
# ========== 添加/修改某次费用 ==========
|
||||
elif action == 'add_feiyong':
|
||||
ch_id = request.data.get('chenghao_id')
|
||||
cishu = request.data.get('cishu')
|
||||
feiyong = request.data.get('feiyong', 0)
|
||||
if not ch_id or not cishu:
|
||||
return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
|
||||
try:
|
||||
ch = Chenghao.query.get(id=ch_id)
|
||||
except Chenghao.DoesNotExist:
|
||||
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||||
if int(cishu) < 1:
|
||||
return Response({'code': 1, 'msg': '次数必须>=1'}, status=400)
|
||||
KaoheCishuFeiyong.query.update_or_create(
|
||||
chenghao=ch, cishu=cishu,
|
||||
defaults={'feiyong': feiyong}
|
||||
)
|
||||
return Response({'code': 0, 'msg': '添加/修改费用成功'})
|
||||
|
||||
# ========== 删除某次费用 ==========
|
||||
elif action == 'remove_feiyong':
|
||||
ch_id = request.data.get('chenghao_id')
|
||||
cishu = request.data.get('cishu')
|
||||
if not ch_id or not cishu:
|
||||
return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
|
||||
if int(cishu) == 1:
|
||||
return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400)
|
||||
try:
|
||||
ch = Chenghao.query.get(id=ch_id)
|
||||
except Chenghao.DoesNotExist:
|
||||
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||||
KaoheCishuFeiyong.query.filter(chenghao=ch, cishu=cishu).delete()
|
||||
return Response({'code': 0, 'msg': '删除成功'})
|
||||
|
||||
# ========== 修改某次费用(等价于add) ==========
|
||||
elif action == 'update_feiyong':
|
||||
return self.add_feiyong(request) # 复用逻辑
|
||||
|
||||
else:
|
||||
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("ChzsgcView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
# ==================== 后端 views.py ====================
|
||||
# 请确保已有以下导入,如果没有请在文件头部添加
|
||||
|
||||
|
||||
|
||||
|
||||
170
backend/views/withdraw_settings.py
Normal file
170
backend/views/withdraw_settings.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
class GetWithdrawSettingsView(APIView):
|
||||
"""
|
||||
获取提现设置(手续费利率、每日限额、当日已提现总额)
|
||||
权限:需要 5500a 或 5500b 或 5500c 任一
|
||||
POST /houtai/hthqtxpz
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
# 需要5500a/b/c任一
|
||||
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
|
||||
return Response({'code': 403, 'msg': '无权限查看提现设置'})
|
||||
|
||||
from jituan.services.withdraw_config import build_withdraw_settings_payload
|
||||
return Response({'code': 0, 'data': build_withdraw_settings_payload(request)})
|
||||
|
||||
|
||||
class UpdateWithdrawSettingsView(APIView):
|
||||
"""
|
||||
修改提现设置(权限细分):
|
||||
- 修改打手相关(利率、个人限额、总限额)需要5500a
|
||||
- 修改管事相关需要5500b
|
||||
- 修改组长相关需要5500c
|
||||
注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算),
|
||||
后端不直接修改 WithdrawalDailyStats,该表由提现流程自动累加。
|
||||
POST /houtai/htxgtxsz
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
# 权限映射:要修改的角色需要对应的权限
|
||||
# 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3
|
||||
from jituan.services.withdraw_config import apply_withdraw_settings_update
|
||||
return apply_withdraw_settings_update(request, permissions)
|
||||
|
||||
|
||||
|
||||
835
backend/views/zuzhang.py
Normal file
835
backend/views/zuzhang.py
Normal file
@@ -0,0 +1,835 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
class GetZuzhangListView(APIView):
|
||||
"""
|
||||
组长列表接口(分页 + 多条件筛选)
|
||||
路由:POST /houtai/hthqzzlb
|
||||
权限:需要登录,且拥有 6600a~6600e 任一权限
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处)
|
||||
def post(self, request):
|
||||
# 1. 获取前端传递的 username(用于防越权)
|
||||
frontend_username = request.data.get('username', '')
|
||||
|
||||
# 2. 调用公共校验方法,验证身份并获取权限列表
|
||||
result = verify_kefu_permission(request, username_from_frontend=frontend_username)
|
||||
|
||||
# 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response)
|
||||
# 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response
|
||||
if isinstance(result, tuple) and len(result) == 2:
|
||||
user_obj, second = result
|
||||
if isinstance(second, Response):
|
||||
return second # 校验失败,直接返回错误响应
|
||||
else:
|
||||
permissions = second # 正常情况:permissions 是列表
|
||||
else:
|
||||
# 防止意外返回格式,视为失败
|
||||
return Response({'code': 500, 'msg': '权限校验模块异常'})
|
||||
|
||||
# 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一)
|
||||
required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e']
|
||||
if not any(perm in permissions for perm in required_perms):
|
||||
logger.warning(f"用户 {request.user.Phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}")
|
||||
return Response({'code': 403, 'msg': '您无权访问组长管理页面'})
|
||||
|
||||
# 4. 提取请求参数(分页 + 筛选)
|
||||
try:
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 20))
|
||||
except ValueError:
|
||||
page, page_size = 1, 20
|
||||
|
||||
keyword = request.data.get('keyword', '').strip()
|
||||
status = request.data.get('status') # 1=正常, 0=禁用
|
||||
invite_count_op = request.data.get('invite_count_op') # gte / lte
|
||||
invite_count_value = request.data.get('invite_count_value')
|
||||
amount_op = request.data.get('amount_op')
|
||||
amount_value = request.data.get('amount_value')
|
||||
commission_op = request.data.get('commission_op')
|
||||
commission_value = request.data.get('commission_value')
|
||||
|
||||
# 5. 构建基础 QuerySet(预加载关联表)
|
||||
queryset = filter_user_related_by_club(
|
||||
UserZuzhang.query.select_related('user', 'user__BossProfile').all(),
|
||||
request,
|
||||
)
|
||||
|
||||
# 6. 应用筛选条件(使用 ORM 安全过滤)
|
||||
if keyword:
|
||||
queryset = queryset.filter(
|
||||
Q(user__UserUID__icontains=keyword) |
|
||||
Q(user__BossProfile__nickname__icontains=keyword)
|
||||
)
|
||||
|
||||
if status is not None and status != '':
|
||||
try:
|
||||
queryset = queryset.filter(zhuangtai=int(status))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 邀请总数筛选
|
||||
if invite_count_value not in (None, ''):
|
||||
try:
|
||||
val = float(invite_count_value)
|
||||
op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte'
|
||||
queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val})
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 可提现金额筛选
|
||||
if amount_value not in (None, ''):
|
||||
try:
|
||||
val = float(amount_value)
|
||||
op = amount_op if amount_op in ('gte', 'lte') else 'gte'
|
||||
queryset = queryset.filter(**{f'ketixian_jine__{op}': val})
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 分佣总额筛选
|
||||
if commission_value not in (None, ''):
|
||||
try:
|
||||
val = float(commission_value)
|
||||
op = commission_op if commission_op in ('gte', 'lte') else 'gte'
|
||||
queryset = queryset.filter(**{f'fenyong_zonge__{op}': val})
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 7. 总数统计
|
||||
total = queryset.count()
|
||||
|
||||
# 8. 分页
|
||||
offset = (page - 1) * page_size
|
||||
paged_queryset = queryset[offset:offset + page_size]
|
||||
|
||||
# 9. 构造返回数据
|
||||
data_list = []
|
||||
for zu in paged_queryset:
|
||||
user = zu.user
|
||||
nickname = ''
|
||||
if hasattr(user, 'BossProfile') and user.BossProfile:
|
||||
nickname = user.BossProfile.nickname or ''
|
||||
|
||||
data_list.append({
|
||||
'yonghuid': user.UserUID,
|
||||
'avatar': user.Avatar or '',
|
||||
'nickname': nickname,
|
||||
'fenyong_zonge': float(zu.fenyong_zonge),
|
||||
'ketixian_jine': float(zu.ketixian_jine),
|
||||
'yaoqing_zongshu': zu.yaoqing_zongshu,
|
||||
'zhuangtai': zu.zhuangtai,
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
**list_response_meta(request),
|
||||
'data': {
|
||||
'list': data_list,
|
||||
'total': total,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class GetZuzhangDetailView(APIView):
|
||||
"""
|
||||
获取组长详情
|
||||
权限:6600a,6600b,6600c,6600d,6600e 任一
|
||||
POST /houtai/hthqzzxq
|
||||
参数:username, yonghuid
|
||||
返回:
|
||||
code:0, data: {
|
||||
base: {...},
|
||||
balance: {...},
|
||||
permanent_enabled, permanent_amount,
|
||||
custom_first_dividend: {},
|
||||
extra_dividend_map: {},
|
||||
all_members: []
|
||||
}
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
yonghuid = request.data.get('yonghuid', '').strip()
|
||||
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
if not yonghuid:
|
||||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||
|
||||
# 权限校验(需要任一权限)
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'}
|
||||
if not any(p in permissions for p in required_perms):
|
||||
return Response({'code': 403, 'msg': '您没有权限查看组长详情'})
|
||||
|
||||
# 查询组长主表、扩展表、老板扩展表(昵称)
|
||||
try:
|
||||
user = User.query.select_related('ZuzhangProfile', 'BossProfile').get(UserUID=yonghuid)
|
||||
zuzhang = user.ZuzhangProfile
|
||||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||
except User.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '组长不存在'})
|
||||
except UserZuzhang.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不是组长'})
|
||||
|
||||
denied = forbid_if_user_out_of_scope(request, user)
|
||||
if denied:
|
||||
return denied
|
||||
|
||||
# 基础信息
|
||||
base_data = {
|
||||
'yonghuid': user.UserUID,
|
||||
'nickname': boss.nickname if boss else '',
|
||||
'avatar': user.Avatar or '',
|
||||
'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从User关联?实际上组长表无电话字段,这里留空或从其他表获取
|
||||
'wechat': '',
|
||||
'zhuangtai': zuzhang.zhuangtai,
|
||||
'yaoqingma': zuzhang.yaoqingma,
|
||||
'yaoqingren': '', # 组长表没有邀请人字段
|
||||
'haibao_url': zuzhang.haibao_url or '',
|
||||
'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
|
||||
'jinri_fenyong': str(zuzhang.jinri_fenyong),
|
||||
'jinyue_fenyong': str(zuzhang.jinyue_fenyong),
|
||||
'fenyong_zonge': str(zuzhang.fenyong_zonge),
|
||||
'CreateTime': zuzhang.CreateTime.isoformat() if zuzhang.CreateTime else None,
|
||||
'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
|
||||
}
|
||||
|
||||
# 余额提现相关
|
||||
balance_data = {
|
||||
'ketixian_jine': str(zuzhang.ketixian_jine),
|
||||
'jinri_tixian': str(zuzhang.jinri_tixian),
|
||||
'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None,
|
||||
'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian,
|
||||
'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane),
|
||||
}
|
||||
|
||||
# 永久分红(额外分红开关和金额)
|
||||
permanent_enabled = zuzhang.kaioi_ewai_fenhong
|
||||
permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0'
|
||||
|
||||
# 获取所有会员(用于前端选择)
|
||||
all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc'))
|
||||
for m in all_members:
|
||||
m['jiage'] = str(m['jiage'])
|
||||
m['zuzhangfc'] = str(m['zuzhangfc'])
|
||||
|
||||
# 获取多次分红配置(按次数和会员组织,按俱乐部隔离)
|
||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||
duoci_records = DuociFenhong.query.filter(
|
||||
yonghuid=yonghuid, club_id=zuzhang_club,
|
||||
).order_by('cishu')
|
||||
custom_first = {} # 首次分红定制(cishu=1)
|
||||
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } }
|
||||
|
||||
# 批量预取会员信息
|
||||
_huiyuan_ids = [r.huiyuan for r in duoci_records]
|
||||
_huiyuan_map = {
|
||||
h.huiyuan_id: h
|
||||
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao', 'jiage')
|
||||
}
|
||||
|
||||
for record in duoci_records:
|
||||
# 获取会员信息
|
||||
member = _huiyuan_map.get(record.huiyuan)
|
||||
if not member:
|
||||
continue
|
||||
member_info = {
|
||||
'jieshao': member.jieshao,
|
||||
'jiage': str(member.jiage),
|
||||
}
|
||||
if record.cishu == 1:
|
||||
custom_first[record.huiyuan] = float(record.zuzhang_fenhong)
|
||||
else:
|
||||
times = record.cishu
|
||||
if times not in extra_map:
|
||||
extra_map[times] = {}
|
||||
extra_map[times][record.huiyuan] = {
|
||||
'zuzhang_fenhong': float(record.zuzhang_fenhong),
|
||||
**member_info
|
||||
}
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'base': base_data,
|
||||
'balance': balance_data,
|
||||
'permanent_enabled': permanent_enabled,
|
||||
'permanent_amount': permanent_amount,
|
||||
'custom_first_dividend': custom_first,
|
||||
'extra_dividend_map': extra_map,
|
||||
'all_members': all_members,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
class UpdateZuzhangView(APIView):
|
||||
"""
|
||||
修改组长信息(基础信息、余额、提现限额、分红策略)
|
||||
权限细分:
|
||||
- 增加可提现金额:6600a
|
||||
- 减少可提现金额:6600b
|
||||
- 修改状态(封禁/解封):6600c
|
||||
- 修改提现限额(kaioi_ewai_tixian/ewai_tixian_xiane):6600d
|
||||
- 修改分红相关(永久分红、定制首次、额外次数):6600e
|
||||
POST /houtai/htxgzzxx
|
||||
参数:
|
||||
username, yonghuid,
|
||||
nickname, avatar, dianhua, wechat, zhuangtai, # 基础
|
||||
ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现
|
||||
permanent_enabled, permanent_amount, # 永久分红
|
||||
custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额}
|
||||
extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}}
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
yonghuid = request.data.get('yonghuid', '').strip()
|
||||
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
if not yonghuid:
|
||||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||
|
||||
# 权限校验
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
# 查询组长对象
|
||||
try:
|
||||
user = User.query.get(UserUID=yonghuid)
|
||||
zuzhang = user.ZuzhangProfile
|
||||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||
except User.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不存在'})
|
||||
except UserZuzhang.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不是组长'})
|
||||
|
||||
# 获取原始数据(用于对比金额变化)
|
||||
old_ketixian_jine = zuzhang.ketixian_jine
|
||||
|
||||
# 开始事务
|
||||
with transaction.atomic():
|
||||
operator = kefu.user.Phone
|
||||
old_nickname = boss.nickname if boss else ''
|
||||
old_zhuangtai = zuzhang.zhuangtai
|
||||
# ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)----------
|
||||
nickname = request.data.get('nickname', '').strip()
|
||||
avatar = request.data.get('avatar', '').strip()
|
||||
dianhua = request.data.get('dianhua', '').strip()
|
||||
wechat = request.data.get('wechat', '').strip()
|
||||
zhuangtai = request.data.get('zhuangtai')
|
||||
base_changes = []
|
||||
# 状态修改需要6600c
|
||||
if zhuangtai is not None:
|
||||
if '6600c' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'})
|
||||
try:
|
||||
zt = int(zhuangtai)
|
||||
if zt in (0, 1):
|
||||
zuzhang.zhuangtai = zt
|
||||
if zt != old_zhuangtai:
|
||||
zt_map = {0: '禁用', 1: '正常'}
|
||||
base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}→{zt_map.get(zt, zt)}')
|
||||
except:
|
||||
pass
|
||||
if nickname:
|
||||
if not boss:
|
||||
boss = UserBoss.query.create(user=user, nickname=nickname)
|
||||
else:
|
||||
boss.nickname = nickname
|
||||
boss.save()
|
||||
if nickname != old_nickname:
|
||||
base_changes.append(f'昵称:{old_nickname or "无"}→{nickname}')
|
||||
if avatar:
|
||||
user.Avatar = avatar
|
||||
user.save()
|
||||
base_changes.append('更新了头像')
|
||||
if base_changes:
|
||||
zuzhang.save()
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=operator,
|
||||
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||||
qitashuoming=(
|
||||
f'【后台】修改组长基础信息:{";".join(base_changes)},'
|
||||
f'组长ID={yonghuid},操作人={operator}'
|
||||
),
|
||||
)
|
||||
# 组长表没有电话/微信字段,如需存储可暂存到User或忽略
|
||||
# 这里不处理
|
||||
|
||||
# ---------- 2. 可提现金额修改(加:6600a;减:6600b)----------
|
||||
new_ketixian_jine = request.data.get('ketixian_jine')
|
||||
if new_ketixian_jine is not None:
|
||||
try:
|
||||
new_val = Decimal(str(new_ketixian_jine))
|
||||
if new_val < 0:
|
||||
return Response({'code': 400, 'msg': '可提现金额不能为负数'})
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||||
if new_val > old_ketixian_jine:
|
||||
if '6600a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'})
|
||||
from jituan.services.group_finance_gate import deny_group_finance_exec
|
||||
deny = deny_group_finance_exec(request.user, request)
|
||||
if deny:
|
||||
return deny
|
||||
action_desc = f'增加{new_val - old_ketixian_jine}元'
|
||||
elif new_val < old_ketixian_jine:
|
||||
if '6600b' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'})
|
||||
action_desc = f'减少{old_ketixian_jine - new_val}元'
|
||||
else:
|
||||
action_desc = '无变动'
|
||||
if new_val != old_ketixian_jine:
|
||||
zuzhang.ketixian_jine = new_val
|
||||
zuzhang.save(update_fields=['ketixian_jine'])
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=operator,
|
||||
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||||
xiugaitijiao=new_val,
|
||||
xiugaitijiaoq=old_ketixian_jine,
|
||||
qitashuoming=(
|
||||
f'【后台】修改组长可提现金额:{old_ketixian_jine}元 → {new_val}元({action_desc}),'
|
||||
f'组长ID={yonghuid},操作人={operator}'
|
||||
),
|
||||
)
|
||||
|
||||
# ---------- 3. 提现限额修改(需要6600d)----------
|
||||
kaioi_ewai = request.data.get('kaioi_ewai_tixian')
|
||||
ewai_val = request.data.get('ewai_tixian_xiane')
|
||||
if kaioi_ewai is not None or ewai_val is not None:
|
||||
if '6600d' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'})
|
||||
limit_changes = []
|
||||
old_kaioi = zuzhang.kaioi_ewai_tixian
|
||||
old_ewai = zuzhang.ewai_tixian_xiane
|
||||
if kaioi_ewai is not None:
|
||||
zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai)
|
||||
if bool(kaioi_ewai) != old_kaioi:
|
||||
limit_changes.append(f'额外限额开关:{"开" if old_kaioi else "关"}→{"开" if kaioi_ewai else "关"}')
|
||||
if ewai_val is not None:
|
||||
try:
|
||||
zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val))
|
||||
if zuzhang.ewai_tixian_xiane != old_ewai:
|
||||
limit_changes.append(f'额外限额金额:{old_ewai}→{zuzhang.ewai_tixian_xiane}')
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
|
||||
zuzhang.save()
|
||||
if limit_changes:
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=operator,
|
||||
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||||
qitashuoming=(
|
||||
f'【后台】修改组长提现限额:{";".join(limit_changes)},'
|
||||
f'组长ID={yonghuid},操作人={operator}'
|
||||
),
|
||||
)
|
||||
|
||||
# ---------- 4. 分红相关修改(需要6600e)----------
|
||||
perm_enabled = request.data.get('permanent_enabled')
|
||||
perm_amount = request.data.get('permanent_amount')
|
||||
custom_first = request.data.get('custom_first_dividend')
|
||||
extra_map = request.data.get('extra_dividend_map')
|
||||
|
||||
if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
|
||||
if '6600e' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'})
|
||||
|
||||
# 4.1 永久分红(额外分红开关和金额)
|
||||
if perm_enabled is not None:
|
||||
zuzhang.kaioi_ewai_fenhong = bool(perm_enabled)
|
||||
if perm_amount is not None:
|
||||
try:
|
||||
zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount))
|
||||
except:
|
||||
return Response({'code': 400, 'msg': '永久分红金额格式错误'})
|
||||
zuzhang.save()
|
||||
|
||||
# 4.2 首次定制分红(cishu=1)
|
||||
if custom_first is not None and isinstance(custom_first, dict):
|
||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||
# 批量预取当前所有首次定制记录(避免循环内 N+1)
|
||||
existing_first_list = list(
|
||||
DuociFenhong.query.filter(
|
||||
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
|
||||
)
|
||||
)
|
||||
_existing_map = {r.huiyuan: r for r in existing_first_list}
|
||||
_to_create = []
|
||||
_to_update = []
|
||||
_to_delete_ids = []
|
||||
for huiyuan_id, amount in custom_first.items():
|
||||
try:
|
||||
amount = Decimal(str(amount))
|
||||
except:
|
||||
continue
|
||||
if amount <= 0:
|
||||
# 删除定制(恢复默认)
|
||||
rec = _existing_map.get(huiyuan_id)
|
||||
if rec is not None:
|
||||
_to_delete_ids.append(rec.id)
|
||||
else:
|
||||
rec = _existing_map.get(huiyuan_id)
|
||||
if rec is not None:
|
||||
rec.zuzhang_fenhong = amount
|
||||
rec.guanshi_fenhong = Decimal('0') # 组长表只关心组长分红
|
||||
_to_update.append(rec)
|
||||
else:
|
||||
_to_create.append(DuociFenhong(
|
||||
club_id=zuzhang_club,
|
||||
yonghuid=yonghuid,
|
||||
huiyuan=huiyuan_id,
|
||||
cishu=1,
|
||||
zuzhang_fenhong=amount,
|
||||
guanshi_fenhong=Decimal('0'),
|
||||
))
|
||||
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
|
||||
for rec in existing_first_list:
|
||||
if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids:
|
||||
_to_delete_ids.append(rec.id)
|
||||
if _to_create:
|
||||
DuociFenhong.objects.bulk_create(_to_create)
|
||||
if _to_update:
|
||||
DuociFenhong.objects.bulk_update(_to_update, ['zuzhang_fenhong', 'guanshi_fenhong'])
|
||||
if _to_delete_ids:
|
||||
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
|
||||
|
||||
# 4.3 额外次数分红(cishu >= 2)
|
||||
if extra_map is not None and isinstance(extra_map, dict):
|
||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||
keep_set = set()
|
||||
_extra_targets = [] # [(cishu, huiyuan_id, amount)]
|
||||
for times_str, members in extra_map.items():
|
||||
try:
|
||||
cishu = int(times_str)
|
||||
if cishu < 2:
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
if not isinstance(members, dict):
|
||||
continue
|
||||
for huiyuan_id, data in members.items():
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
amount = data.get('zuzhang_fenhong')
|
||||
if amount is None:
|
||||
continue
|
||||
try:
|
||||
amount = Decimal(str(amount))
|
||||
except:
|
||||
continue
|
||||
keep_set.add((cishu, huiyuan_id))
|
||||
_extra_targets.append((cishu, huiyuan_id, amount))
|
||||
# 批量预取已有额外次数记录
|
||||
existing_extra_list = list(
|
||||
DuociFenhong.query.filter(
|
||||
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
|
||||
)
|
||||
)
|
||||
_extra_map = {(r.cishu, r.huiyuan): r for r in existing_extra_list}
|
||||
_to_create = []
|
||||
_to_update = []
|
||||
_to_delete_ids = []
|
||||
for cishu, huiyuan_id, amount in _extra_targets:
|
||||
rec = _extra_map.get((cishu, huiyuan_id))
|
||||
if rec is not None:
|
||||
rec.zuzhang_fenhong = amount
|
||||
rec.guanshi_fenhong = Decimal('0')
|
||||
_to_update.append(rec)
|
||||
else:
|
||||
_to_create.append(DuociFenhong(
|
||||
club_id=zuzhang_club,
|
||||
yonghuid=yonghuid,
|
||||
huiyuan=huiyuan_id,
|
||||
cishu=cishu,
|
||||
zuzhang_fenhong=amount,
|
||||
guanshi_fenhong=Decimal('0'),
|
||||
))
|
||||
# 删除不在 keep_set 中的额外次数记录
|
||||
for rec in existing_extra_list:
|
||||
if (rec.cishu, rec.huiyuan) not in keep_set:
|
||||
_to_delete_ids.append(rec.id)
|
||||
if _to_create:
|
||||
DuociFenhong.objects.bulk_create(_to_create)
|
||||
if _to_update:
|
||||
DuociFenhong.objects.bulk_update(_to_update, ['zuzhang_fenhong', 'guanshi_fenhong'])
|
||||
if _to_delete_ids:
|
||||
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
|
||||
|
||||
dividend_parts = []
|
||||
if perm_enabled is not None:
|
||||
dividend_parts.append(f'永久分红开关→{"开" if perm_enabled else "关"}')
|
||||
if perm_amount is not None:
|
||||
dividend_parts.append(f'永久分红金额→{perm_amount}')
|
||||
if custom_first is not None:
|
||||
dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)')
|
||||
if extra_map is not None:
|
||||
dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)')
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=operator,
|
||||
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||||
qitashuoming=(
|
||||
f'【后台】修改组长分红策略:{";".join(dividend_parts) or "配置已更新"},'
|
||||
f'组长ID={yonghuid},操作人={operator}'
|
||||
),
|
||||
)
|
||||
|
||||
# 保存组长其他可能修改的字段(例如电话、微信没有,忽略)
|
||||
|
||||
return Response({'code': 0, 'msg': '修改成功'})
|
||||
|
||||
|
||||
class ChangeInviterView(APIView):
|
||||
"""
|
||||
后台更换邀请人/上级
|
||||
POST /houtai/htghyqr
|
||||
参数:
|
||||
username: 操作人账号
|
||||
yonghuid: 被修改用户ID
|
||||
target_type: dashou(打手)| guanshi(管事)
|
||||
new_yaoqingren: 新邀请人用户ID(须在 User 主表存在)
|
||||
权限:
|
||||
dashou → 001ee
|
||||
guanshi → 4400a
|
||||
校验:
|
||||
打手新上级须为管事;管事新上级须为组长;不可设为自己
|
||||
"""
|
||||
permission_classes = []
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
yonghuid = request.data.get('yonghuid', '').strip()
|
||||
target_type = request.data.get('target_type', '').strip().lower()
|
||||
new_yaoqingren = request.data.get('new_yaoqingren', '').strip()
|
||||
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
if not yonghuid:
|
||||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||
if target_type not in ('dashou', 'guanshi'):
|
||||
return Response({'code': 400, 'msg': 'target_type 须为 dashou 或 guanshi'})
|
||||
if not new_yaoqingren:
|
||||
return Response({'code': 400, 'msg': '缺少新邀请人ID'})
|
||||
if new_yaoqingren == yonghuid:
|
||||
return Response({'code': 400, 'msg': '不能将自己设为邀请人'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
|
||||
if target_type == 'dashou':
|
||||
if '001ee' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限更换打手邀请人(需要001ee)'})
|
||||
else:
|
||||
if '4400a' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限更换管事邀请人(需要4400a)'})
|
||||
|
||||
try:
|
||||
target_user = User.query.get(UserUID=yonghuid)
|
||||
except User.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '被修改用户不存在'})
|
||||
|
||||
try:
|
||||
inviter_user = User.query.get(UserUID=new_yaoqingren)
|
||||
except User.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '新邀请人用户ID不存在'})
|
||||
|
||||
operator = kefu.user.Phone
|
||||
|
||||
with transaction.atomic():
|
||||
if target_type == 'dashou':
|
||||
try:
|
||||
dashou = target_user.DashouProfile
|
||||
except UserDashou.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不是打手'})
|
||||
try:
|
||||
guanshi_profile = inviter_user.GuanshiProfile
|
||||
except UserGuanshi.DoesNotExist:
|
||||
return Response({'code': 400, 'msg': '新邀请人不是管事,无法作为打手上级'})
|
||||
if guanshi_profile.zhuangtai != 1:
|
||||
return Response({'code': 400, 'msg': '新邀请人管事账号已禁用'})
|
||||
old_inviter = dashou.yaoqingren or '无'
|
||||
dashou.yaoqingren = new_yaoqingren
|
||||
dashou.save(update_fields=['yaoqingren'])
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=operator,
|
||||
leixing=XIUGAI_LEIXING_DASHOU,
|
||||
qitashuoming=(
|
||||
f'【后台】更换打手邀请人:{old_inviter} → {new_yaoqingren},'
|
||||
f'打手ID={yonghuid},新上级须为管事(已校验),操作人={operator}'
|
||||
),
|
||||
)
|
||||
else:
|
||||
try:
|
||||
guanshi = target_user.GuanshiProfile
|
||||
except UserGuanshi.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '用户不是管事'})
|
||||
try:
|
||||
zuzhang_profile = inviter_user.ZuzhangProfile
|
||||
except UserZuzhang.DoesNotExist:
|
||||
return Response({'code': 400, 'msg': '新邀请人不是组长,无法作为管事上级'})
|
||||
if zuzhang_profile.zhuangtai != 1:
|
||||
return Response({'code': 400, 'msg': '新邀请人组长账号已禁用'})
|
||||
old_inviter = guanshi.yaoqingren or '无'
|
||||
guanshi.yaoqingren = new_yaoqingren
|
||||
guanshi.save(update_fields=['yaoqingren'])
|
||||
write_xiugai_log(
|
||||
yonghuid=yonghuid,
|
||||
xiugaiid=operator,
|
||||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||
qitashuoming=(
|
||||
f'【后台】更换管事邀请人:{old_inviter} → {new_yaoqingren},'
|
||||
f'管事ID={yonghuid},新上级须为组长(已校验),操作人={operator}'
|
||||
),
|
||||
)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '邀请人更换成功',
|
||||
'data': {'yonghuid': yonghuid, 'new_yaoqingren': new_yaoqingren},
|
||||
})
|
||||
|
||||
|
||||
347
backend/views/zxkf.py
Normal file
347
backend/views/zxkf.py
Normal file
@@ -0,0 +1,347 @@
|
||||
import hmac
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
import hashlib
|
||||
import xmltodict
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
|
||||
# 工具类导入
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action,
|
||||
update_shangjia_daily
|
||||
)
|
||||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||
from jituan.services.club_penalty import (
|
||||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||
)
|
||||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from ..utils import (
|
||||
verify_kefu_permission,
|
||||
PERMISSION_TO_SHENFEN,
|
||||
SHENFEN_PROFILE_MAP,
|
||||
has_fadan_view_permission,
|
||||
has_merchant_order_permission,
|
||||
check_fadan_permission,
|
||||
pick_order_id,
|
||||
pick_penalty_create_params,
|
||||
write_xiugai_log,
|
||||
XIUGAI_LEIXING_DASHOU,
|
||||
XIUGAI_LEIXING_GUANSHI,
|
||||
XIUGAI_LEIXING_SHANGJIA,
|
||||
XIUGAI_LEIXING_ZUZHANG,
|
||||
XIUGAI_LEIXING_LABEL,
|
||||
)
|
||||
|
||||
# models 集中导入
|
||||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||
## backend
|
||||
from backend.models import WithdrawalDailyStats
|
||||
|
||||
## users
|
||||
from users.models import (
|
||||
UserGuanshi, UserBoss, UserZuzhang,
|
||||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
## orders
|
||||
from orders.models import (
|
||||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||
OrderPlayerHistory, Order, RefundRecord,
|
||||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||
)
|
||||
|
||||
## shop
|
||||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||
|
||||
## products
|
||||
from products.models import (
|
||||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||
)
|
||||
|
||||
## config
|
||||
from config.models import (
|
||||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||
TixianQuotaDefault,
|
||||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||
)
|
||||
|
||||
## rank
|
||||
from rank.models import (
|
||||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||
)
|
||||
|
||||
# 序列化器
|
||||
from ..serializers import PopupPageSerializer
|
||||
|
||||
# 全局常量、日志对象
|
||||
logger = logging.getLogger('houtai')
|
||||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||
|
||||
class ZxkfghdsView(APIView):
|
||||
"""
|
||||
客服更换打手接口
|
||||
路径:POST /houtai/zxkfghds
|
||||
功能:
|
||||
1. 将当前订单状态改为已退款(5),释放接单打手(若有)
|
||||
2. 如需跨平台通知,则调用对方平台退款通知接口
|
||||
3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写)
|
||||
4. 生成新的派单链接,并标记为已使用
|
||||
5. 新链接记录上一个链接ID和上一个订单ID
|
||||
6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款
|
||||
权限:002ac(商家订单管理)/ 002ab(平台订单管理)/ 003aa(跨平台管理)
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
# ========== 1. 参数提取 ==========
|
||||
username = request.data.get('username', '').strip() or request.data.get('phone', '').strip()
|
||||
OrderID = pick_order_id(request.data)
|
||||
if not username or not OrderID:
|
||||
logger.warning("更换打手参数不完整")
|
||||
return Response({'code': 400, 'msg': '参数不完整'})
|
||||
|
||||
# ========== 2. 权限校验(商家链接页客服需 002ac,非仅跨平台 003aa) ==========
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions # 返回 error response
|
||||
if not has_merchant_order_permission(permissions):
|
||||
return Response({'code': 403, 'msg': '您无权处理订单'})
|
||||
|
||||
current_user = request.user
|
||||
|
||||
# ========== 3. 查询原订单及关联信息 ==========
|
||||
try:
|
||||
old_order = Order.query.select_related('shangjia_kuozhan').get(OrderID=OrderID)
|
||||
except Order.DoesNotExist:
|
||||
logger.warning(f"订单不存在: {OrderID}")
|
||||
return Response({'code': 404, 'msg': '订单不存在'})
|
||||
|
||||
# 仅允许状态为 2(进行中)或 8(结算中)更换打手
|
||||
if old_order.Status not in [1,7,2, 8]:
|
||||
return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'})
|
||||
|
||||
# 本接口仅处理商家发单(Platform=2)
|
||||
if old_order.Platform != 2:
|
||||
return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'})
|
||||
|
||||
# 获取原链接(用于模板ID等)
|
||||
try:
|
||||
old_lianjie = ShangjiaLianjie.query.get(OrderID=OrderID)
|
||||
except ShangjiaLianjie.DoesNotExist:
|
||||
logger.error(f"原链接记录不存在,订单: {OrderID}")
|
||||
return Response({'code': 500, 'msg': '链接数据异常'})
|
||||
|
||||
# 商家信息(仅用于记录关联,不操作余额)
|
||||
try:
|
||||
shangjia_ext = old_order.shangjia_kuozhan
|
||||
shangjia_id = shangjia_ext.MerchantID
|
||||
# 获取商家昵称用于新订单扩展表
|
||||
shangjia_user = User.query.select_related('ShopProfile').get(UserUID=shangjia_id)
|
||||
shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}"
|
||||
except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
|
||||
logger.error(f"商家信息缺失,订单: {OrderID}")
|
||||
return Response({'code': 500, 'msg': '商家数据异常'})
|
||||
|
||||
# ========== 4. 退款处理(仅打手侧,不碰商家) ==========
|
||||
with transaction.atomic():
|
||||
# 5.1 订单状态改为已退款
|
||||
old_order.Status = 5
|
||||
old_order.AssignedCS = current_user.Phone
|
||||
old_order.save()
|
||||
|
||||
# 5.2 处理接单打手(若有)
|
||||
dashou_id = old_order.PlayerID
|
||||
if dashou_id:
|
||||
try:
|
||||
dashou_user = User.query.get(UserUID=dashou_id)
|
||||
dashou_profile = dashou_user.DashouProfile
|
||||
dashou_profile.tuikuanliang += 1 # 退款次数+1
|
||||
dashou_profile.zhuangtai = 1 # 设为空闲
|
||||
dashou_profile.save()
|
||||
except (User.DoesNotExist, ObjectDoesNotExist):
|
||||
logger.warning(f"打手 {dashou_id} 不存在,跳过更新")
|
||||
|
||||
# 5.3 更新退款记录表
|
||||
RefundRecord.query.update_or_create(
|
||||
OrderID=OrderID,
|
||||
defaults={
|
||||
'PlayerID': dashou_id or '',
|
||||
'ApplicantID': '',
|
||||
'ProcessorID': current_user.Phone,
|
||||
'Reason': '客服更换打手退款',
|
||||
'ApplyStatus': 1,
|
||||
'Amount': old_order.Amount or 0,
|
||||
'PlayerCommission': old_order.PlayerCommission or 0,
|
||||
'Description': old_order.Description or '',
|
||||
'Remark': '更换打手自动退款',
|
||||
'Nickname': old_order.Nickname or '',
|
||||
}
|
||||
)
|
||||
|
||||
# 5.4 打手每日统计(退款)
|
||||
if dashou_id:
|
||||
try:
|
||||
update_dashou_daily_by_action(
|
||||
yonghuid=dashou_id,
|
||||
amount=old_order.PlayerCommission,
|
||||
action=3 # 3=退款
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"打手每日统计更新失败: {e}")
|
||||
|
||||
# 5.5 客服处理统计(管理员无 KefuProfile,跳过)
|
||||
if hasattr(kefu, 'jinrichuli'):
|
||||
kefu.jinrichuli += 1
|
||||
kefu.jinyuechuli += 1
|
||||
kefu.zongchuli += 1
|
||||
kefu.save()
|
||||
|
||||
# 至此,原订单已退款,事务提交
|
||||
|
||||
# ========== 6. 生成新订单和链接(不扣商家余额) ==========
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# ---- 6.1 生成新的订单ID和Token ----
|
||||
def generate_OrderID():
|
||||
timestamp = int(time.time() * 1000)
|
||||
process_id = os.getpid() % 1000
|
||||
random_num = random.randint(1000, 9999)
|
||||
return f"SJ{timestamp}{process_id:03d}{random_num}"
|
||||
|
||||
def generate_secure_token(dd_id, uid):
|
||||
timestamp = int(time.time() * 1_000_000)
|
||||
salt = secrets.token_hex(8)
|
||||
raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
|
||||
token = hashlib.sha256(raw.encode()).hexdigest()[:24]
|
||||
if ShangjiaLianjie.query.filter(LinkToken=token).exists():
|
||||
return generate_secure_token(dd_id, uid)
|
||||
return token
|
||||
|
||||
def generate_link_url(token):
|
||||
base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
|
||||
return f"{base}/order/{token}"
|
||||
|
||||
new_OrderID = generate_OrderID()
|
||||
secure_token = generate_secure_token(new_OrderID, shangjia_id)
|
||||
|
||||
# ---- 6.2 创建新订单(复制核心数据,状态直接为1) ----
|
||||
new_order = Order.query.create(
|
||||
OrderID=new_OrderID,
|
||||
Status=1, # 下单中
|
||||
Platform=2,
|
||||
Amount=old_order.Amount,
|
||||
PlayerCommission=old_order.PlayerCommission,
|
||||
ProductTypeID=old_order.ProductTypeID,
|
||||
GrabRequirement=old_order.GrabRequirement,
|
||||
MembershipID=old_order.MembershipID,
|
||||
CommissionReq=old_order.CommissionReq,
|
||||
Description=old_order.Description,
|
||||
Nickname=old_order.Nickname, # 继承游戏昵称
|
||||
Remark=old_order.Remark,
|
||||
User1ID=old_order.User1ID, # 商家标识
|
||||
ImageURL=old_order.ImageURL,
|
||||
ExternalOrderID=old_order.ExternalOrderID,
|
||||
ClubID=getattr(old_order, 'ClubID', None) or 'xq',
|
||||
# 注意:不继承 AssignedID、PlayerID
|
||||
)
|
||||
|
||||
# ---- 6.3 创建商家扩展表 ----
|
||||
MerchantOrderExt.query.create(
|
||||
Order=new_order,
|
||||
MerchantID=shangjia_id,
|
||||
MerchantNickname=shangjia_nicheng,
|
||||
)
|
||||
|
||||
# ---- 6.4 生成新链接并关联旧链接信息 ----
|
||||
link_url = generate_link_url(secure_token)
|
||||
new_lianjie = ShangjiaLianjie.query.create(
|
||||
UserID=shangjia_id,
|
||||
ProductTypeID=old_lianjie.ProductTypeID,
|
||||
TemplateID=old_lianjie.TemplateID,
|
||||
LinkURL=link_url,
|
||||
LinkToken=secure_token,
|
||||
OrderID=new_OrderID,
|
||||
is_used=True, # 直接标记已使用
|
||||
ExpireTime=timezone.now() + timezone.timedelta(days=1),
|
||||
PreviousLinkID=old_lianjie.id, # 记录旧链接ID
|
||||
PreviousOrderID=old_order.OrderID, # 记录旧订单ID
|
||||
)
|
||||
|
||||
# ---- 6.5 异步通知(广播新订单、跨平台同步) ----
|
||||
try:
|
||||
order_info = {
|
||||
'OrderID': new_OrderID,
|
||||
'game_type': self._get_game_type_name(new_order.ProductTypeID),
|
||||
'amount': str(new_order.Amount),
|
||||
'order_desc': (new_order.Description or '')[:50],
|
||||
}
|
||||
dingdan_guangbo.delay(order_info)
|
||||
except Exception as e:
|
||||
logger.error(f"广播通知失败: {e}")
|
||||
|
||||
# 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单
|
||||
|
||||
logger.info(f"更换打手成功:旧订单{OrderID}→退款,新订单{new_OrderID}已生成")
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '更换成功',
|
||||
'data': {
|
||||
'new_OrderID': new_OrderID,
|
||||
'new_dingdan_id': new_OrderID,
|
||||
'new_lianjie': link_url,
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True)
|
||||
return Response({'code': 500, 'msg': '生成新订单失败'})
|
||||
|
||||
def _get_game_type_name(self, ProductTypeID):
|
||||
"""根据类型ID获取游戏类型名称"""
|
||||
try:
|
||||
gt = ShangpinLeixing.query.filter(id=ProductTypeID).first()
|
||||
return gt.jieshao if gt else '游戏订单'
|
||||
except Exception:
|
||||
return '游戏订单'
|
||||
|
||||
|
||||
@@ -1 +1,16 @@
|
||||
from django.contrib import admin
|
||||
from .models import MiniappScriptScene, MiniappScriptAutoReply
|
||||
|
||||
|
||||
@admin.register(MiniappScriptScene)
|
||||
class MiniappScriptSceneAdmin(admin.ModelAdmin):
|
||||
list_display = ('club_id', 'scene_key', 'title', 'enabled', 'UpdateTime')
|
||||
list_filter = ('club_id', 'enabled')
|
||||
search_fields = ('scene_key', 'title', 'content')
|
||||
|
||||
|
||||
@admin.register(MiniappScriptAutoReply)
|
||||
class MiniappScriptAutoReplyAdmin(admin.ModelAdmin):
|
||||
list_display = ('club_id', 'keyword', 'reply_type', 'sort_order', 'enabled', 'UpdateTime')
|
||||
list_filter = ('club_id', 'reply_type', 'enabled')
|
||||
search_fields = ('keyword', 'reply_content')
|
||||
|
||||
114
config/management/commands/fix_income_month.py
Normal file
114
config/management/commands/fix_income_month.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
修复 repair_wechat_income 搞乱的收入(200 显示成 600+)。
|
||||
|
||||
两种模式(先预览,再加 --yes):
|
||||
|
||||
A. 默认:重扫本月真实微信支付,删掉脏数据后重建
|
||||
python manage.py fix_income_month
|
||||
python manage.py fix_income_month --yes
|
||||
|
||||
B. 若日志金额已对、只是日统计表脏了:
|
||||
python manage.py fix_income_month --logs-only --yes
|
||||
"""
|
||||
from calendar import monthrange
|
||||
from datetime import date
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Sum
|
||||
|
||||
from config.models import DailyIncomeStat, PlatformIncomeLog
|
||||
from jituan.services.szjilu_accounting import (
|
||||
align_daily_income_from_logs_only,
|
||||
dedupe_platform_income_logs,
|
||||
fix_income_period,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '修复本月收入统计错乱(repair 重复记账)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--year', type=int, default=0, help='年,默认今年')
|
||||
parser.add_argument('--month', type=int, default=0, help='月,默认本月')
|
||||
parser.add_argument('--today-only', action='store_true', help='只修今天')
|
||||
parser.add_argument('--logs-only', action='store_true', help='只按现有日志重写日统计,不重扫订单')
|
||||
parser.add_argument('--yes', action='store_true', help='确认执行')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
today = date.today()
|
||||
year = options['year'] or today.year
|
||||
month = options['month'] or today.month
|
||||
dry_run = not options['yes']
|
||||
|
||||
if options['today_only']:
|
||||
start = end = today
|
||||
label = f'今日 {today}'
|
||||
else:
|
||||
_, last = monthrange(year, month)
|
||||
start = date(year, month, 1)
|
||||
end = date(year, month, last)
|
||||
label = f'{year}-{month:02d} 全月'
|
||||
|
||||
if options['logs_only']:
|
||||
self._run_logs_only(dry_run, label)
|
||||
return
|
||||
|
||||
self.stdout.write(self.style.WARNING(f'【{label}】收入修复\n'))
|
||||
if dry_run:
|
||||
self.stdout.write('预览模式(未改库)\n')
|
||||
|
||||
r = fix_income_period(start, end, dry_run=dry_run)
|
||||
self.stdout.write(f' 错乱日统计合计: {r["before_daily_amount"]:.2f} 元 / {r["before_daily_count"]} 笔')
|
||||
self.stdout.write(f' 错乱日志合计: {r["before_log_amount"]:.2f} 元 / {r["before_log_count"]} 笔')
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f' 重建后(严格): {r["after_amount"]:.2f} 元 / {r["after_count"]} 笔'
|
||||
))
|
||||
|
||||
if r['items']:
|
||||
self.stdout.write(' 明细:')
|
||||
for row in r['items']:
|
||||
if len(row) == 5:
|
||||
d, ref, cid, amt, kind = row
|
||||
self.stdout.write(f' {d} {ref} {amt}元 club={cid} ({kind})')
|
||||
else:
|
||||
ref, cid, amt, kind = row
|
||||
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
|
||||
|
||||
if not dry_run:
|
||||
self._print_today_total(today)
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'\n重建后金额对 → python manage.py fix_income_month --yes\n'
|
||||
'只修今天 → python manage.py fix_income_month --today-only --yes\n'
|
||||
'日志已对、日统计脏 → python manage.py fix_income_month --logs-only --yes'
|
||||
))
|
||||
|
||||
def _run_logs_only(self, dry_run, label):
|
||||
try:
|
||||
log_sum = PlatformIncomeLog.objects.aggregate(t=Sum('amount'))['t'] or 0
|
||||
log_cnt = PlatformIncomeLog.objects.count()
|
||||
daily_sum = DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0
|
||||
except Exception as exc:
|
||||
self.stderr.write(f'读取失败: {exc}')
|
||||
return
|
||||
|
||||
self.stdout.write(f'【{label}】仅按日志对齐日统计\n')
|
||||
self.stdout.write(f' platform_income_log 合计: {float(log_sum):.2f} 元 / {log_cnt} 笔')
|
||||
self.stdout.write(f' daily_income_stat 现合计: {float(daily_sum):.2f} 元')
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('\n预览。确认后: python manage.py fix_income_month --logs-only --yes'))
|
||||
return
|
||||
|
||||
n = align_daily_income_from_logs_only()
|
||||
self.stdout.write(self.style.SUCCESS(f'已重写 daily_income_stat {n} 行,并重算 szjilu'))
|
||||
self._print_today_total(date.today())
|
||||
|
||||
def _print_today_total(self, today):
|
||||
agg = DailyIncomeStat.objects.filter(date=today).aggregate(
|
||||
amt=Sum('total_amount'), cnt=Sum('total_count'),
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\n数据库今日日统计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)} 笔'
|
||||
))
|
||||
self.stdout.write('请刷新后台财务页核对。')
|
||||
29
config/management/commands/fix_today_income_count.py
Normal file
29
config/management/commands/fix_today_income_count.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""只修正今日 daily_income_stat.total_count(不动金额、不动其它天)。"""
|
||||
from datetime import date
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from jituan.services.szjilu_accounting import fix_income_stat_count_for_date
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '修正今日收入笔数(repair 会把 total_count 虚增,本命令只改笔数列)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--date', type=str, default='', help='YYYY-MM-DD,默认今天')
|
||||
parser.add_argument('--yes', action='store_true')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
stat_date = date.fromisoformat(options['date']) if options['date'] else date.today()
|
||||
dry_run = not options['yes']
|
||||
r = fix_income_stat_count_for_date(stat_date, dry_run=dry_run)
|
||||
self.stdout.write(f'日期: {r["date"]}')
|
||||
self.stdout.write(f' 表内笔数合计(错): {r["before_count"]}')
|
||||
self.stdout.write(self.style.SUCCESS(f' 修正后笔数: {r["after_count"]}'))
|
||||
if r['clubs']:
|
||||
for cid, cnt in r['clubs'].items():
|
||||
self.stdout.write(f' club={cid}: {cnt} 笔')
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('\n确认: python manage.py fix_today_income_count --yes'))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS('\n已写入 daily_income_stat.total_count,请刷新财务页。'))
|
||||
70
config/management/commands/rebuild_income_stats.py
Normal file
70
config/management/commands/rebuild_income_stats.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
【终极恢复】清空全部收支脏数据,按真实微信支付逐笔重建。
|
||||
|
||||
只执行这一条,不要再用 repair / undo / fix:
|
||||
|
||||
预览:python manage.py rebuild_income_stats
|
||||
执行:python manage.py rebuild_income_stats --yes
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Sum
|
||||
|
||||
from config.models import DailyIncomeStat
|
||||
from jituan.services.szjilu_accounting import rebuild_income_stats_from_scratch
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '清空收支脏数据,按真实微信支付逐笔重建(终极恢复)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--since-year', type=int, default=0, help='从哪一年扫,默认去年')
|
||||
parser.add_argument('--until-year', type=int, default=0, help='到哪一年,默认今年')
|
||||
parser.add_argument('--yes', action='store_true', help='确认执行')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
until_year = options['until_year'] or date.today().year
|
||||
since_year = options['since_year'] or (until_year - 1)
|
||||
dry_run = not options['yes']
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('【预览】未改数据库\n'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(
|
||||
'【执行】删除全部收入日志和日统计,按真实支付重建...\n'
|
||||
))
|
||||
|
||||
r = rebuild_income_stats_from_scratch(
|
||||
since_year=since_year, until_year=until_year, dry_run=dry_run,
|
||||
)
|
||||
|
||||
self.stdout.write(f'=== {since_year} ~ {until_year} ===')
|
||||
self.stdout.write(f' 错乱前 daily 合计: {r["before_daily_total"]:.2f} 元')
|
||||
self.stdout.write(f' 错乱前 log: {r["before_log_total"]:.2f} 元 / {r["before_log_count"]} 条')
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f' 重建后合计: {r["after_total"]:.2f} 元 / {r["after_count"]} 笔'
|
||||
))
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f' 重建后本月: {r["month_amount"]:.2f} 元'
|
||||
))
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f' 重建后今日: {r["today_amount"]:.2f} 元 / {r["today_count"]} 笔 ← 看这条'
|
||||
))
|
||||
|
||||
if not dry_run:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\ndaily 表合计: {r.get("after_daily_total", 0):.2f} 元'
|
||||
))
|
||||
agg = DailyIncomeStat.objects.filter(date=date.today()).aggregate(
|
||||
amt=Sum('total_amount'), cnt=Sum('total_count'),
|
||||
)
|
||||
self.stdout.write(
|
||||
f'今日 daily 行合计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)} 笔'
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS('\n完成。刷新后台财务页。'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f'\n今日 {r["today_amount"]:.2f} 元对的话:\n'
|
||||
f' python manage.py rebuild_income_stats --yes'
|
||||
))
|
||||
104
config/management/commands/recover_config_0006.py
Normal file
104
config/management/commands/recover_config_0006.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""修复 config.0006 半路失败:去重 + 补唯一约束/索引,然后 fake 0006。"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import connection
|
||||
from django.db.models import Max
|
||||
|
||||
from config.models import PopupPage, Tupianpeizhi
|
||||
|
||||
|
||||
def _table_indexes(table):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f'SHOW INDEX FROM `{table}`')
|
||||
rows = cursor.fetchall()
|
||||
# Key_name, Column_name, Non_unique
|
||||
by_key = {}
|
||||
for row in rows:
|
||||
key_name = row[2]
|
||||
col = row[4]
|
||||
non_unique = row[1]
|
||||
by_key.setdefault(key_name, {'cols': [], 'unique': non_unique == 0})
|
||||
by_key[key_name]['cols'].append(col)
|
||||
return by_key
|
||||
|
||||
|
||||
def _has_unique_on(table, columns):
|
||||
cols = list(columns)
|
||||
for info in _table_indexes(table).values():
|
||||
if info['unique'] and info['cols'] == cols:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _drop_index(table, key_name):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f'ALTER TABLE `{table}` DROP INDEX `{key_name}`')
|
||||
|
||||
|
||||
def _add_unique(table, name, columns):
|
||||
cols = ', '.join(f'`{c}`' for c in columns)
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f'ALTER TABLE `{table}` ADD UNIQUE `{name}` ({cols})')
|
||||
|
||||
|
||||
def _add_index(table, name, columns):
|
||||
cols = ', '.join(f'`{c}`' for c in columns)
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f'CREATE INDEX `{name}` ON `{table}` ({cols})')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'config.0006 半路失败时:清重复数据、补约束,再执行 migrate config 0006 --fake'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('1) 清理重复数据…')
|
||||
|
||||
for row in Tupianpeizhi.objects.values('club_id', 'ImageType').annotate(max_id=Max('id')):
|
||||
n = Tupianpeizhi.objects.filter(
|
||||
club_id=row['club_id'],
|
||||
ImageType=row['ImageType'],
|
||||
).exclude(id=row['max_id']).delete()[0]
|
||||
if n:
|
||||
self.stdout.write(f' tupianpeizhi 删除 {n} 条重复')
|
||||
|
||||
for row in PopupPage.objects.values('club_id', 'page_key').annotate(max_id=Max('id')):
|
||||
n = PopupPage.objects.filter(
|
||||
club_id=row['club_id'],
|
||||
page_key=row['page_key'],
|
||||
).exclude(id=row['max_id']).delete()[0]
|
||||
if n:
|
||||
self.stdout.write(f' popup_page 删除 {n} 条重复')
|
||||
|
||||
self.stdout.write('2) 检查并补约束/索引…')
|
||||
|
||||
# popup_page:去掉 page_key 单独唯一,改为 (club_id, page_key)
|
||||
popup_indexes = _table_indexes('popup_page')
|
||||
for key_name, info in popup_indexes.items():
|
||||
if info['unique'] and info['cols'] == ['page_key']:
|
||||
self.stdout.write(f' 删除 popup_page 旧唯一索引 {key_name}')
|
||||
_drop_index('popup_page', key_name)
|
||||
|
||||
if not _has_unique_on('popup_page', ['club_id', 'page_key']):
|
||||
self.stdout.write(' 添加 popup_page UNIQUE (club_id, page_key)')
|
||||
_add_unique('popup_page', 'popup_page_club_page_key_uniq', ['club_id', 'page_key'])
|
||||
else:
|
||||
self.stdout.write(' popup_page UNIQUE (club_id, page_key) 已存在')
|
||||
|
||||
if not _has_unique_on('tupianpeizhi', ['club_id', 'ImageType']):
|
||||
self.stdout.write(' 添加 tupianpeizhi UNIQUE (club_id, ImageType)')
|
||||
_add_unique('tupianpeizhi', 'tupianpeizhi_club_id_ImageType_uniq', ['club_id', 'ImageType'])
|
||||
else:
|
||||
self.stdout.write(' tupianpeizhi UNIQUE (club_id, ImageType) 已存在')
|
||||
|
||||
lunbo_indexes = _table_indexes('lunbo')
|
||||
if 'lunbo_club_page_type_idx' not in lunbo_indexes:
|
||||
self.stdout.write(' 添加 lunbo 索引 lunbo_club_page_type_idx')
|
||||
_add_index('lunbo', 'lunbo_club_page_type_idx', ['club_id', 'page_key', 'ImageType'])
|
||||
else:
|
||||
self.stdout.write(' lunbo 索引 lunbo_club_page_type_idx 已存在')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
'\n完成。请执行:\n'
|
||||
' python manage.py migrate config 0006 --fake\n'
|
||||
' python manage.py migrate config\n'
|
||||
' python manage.py migrate\n'
|
||||
))
|
||||
92
config/management/commands/recover_wechat_income_stats.py
Normal file
92
config/management/commands/recover_wechat_income_stats.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
恢复被 repair_wechat_income 搞乱的收入统计。
|
||||
|
||||
repair 会对已入账订单重复加钱(200 变 900)。
|
||||
本命令:删当日错误日志 → 按真实微信支付重建 → 扣回 szjilu 多记部分。
|
||||
|
||||
用法:
|
||||
1. 先预览:python manage.py recover_wechat_income_stats
|
||||
2. 看「重建后」金额是否对(应接近真实收入,如 200 左右)
|
||||
3. 确认后:python manage.py recover_wechat_income_stats --yes
|
||||
4. 若多日错乱:python manage.py recover_wechat_income_stats --days 7 --yes
|
||||
5. 最后全量对齐日统计表:python manage.py recover_wechat_income_stats --yes --rebuild-all-daily
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Sum
|
||||
|
||||
from config.models import DailyIncomeStat
|
||||
from jituan.services.szjilu_accounting import (
|
||||
rebuild_all_daily_income_from_logs,
|
||||
recover_income_stats_for_date,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '恢复 repair_wechat_income 重复记账(先预览,--yes 执行)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--date', type=str, default='', help='日期 YYYY-MM-DD,默认今天')
|
||||
parser.add_argument('--days', type=int, default=1, help='向前恢复几天')
|
||||
parser.add_argument('--yes', action='store_true', help='确认执行')
|
||||
parser.add_argument(
|
||||
'--rebuild-all-daily',
|
||||
action='store_true',
|
||||
help='恢复后按 platform_income_log 全量重写 daily_income_stat(纠正累计总收入)',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['date']:
|
||||
end_day = date.fromisoformat(options['date'])
|
||||
else:
|
||||
end_day = date.today()
|
||||
days = max(1, int(options['days'] or 1))
|
||||
start_day = end_day - timedelta(days=days - 1)
|
||||
dry_run = not options['yes']
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'【预览】未改数据库。下面「重建后」应是正确金额。\n'
|
||||
))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING('【执行】正在恢复...\n'))
|
||||
|
||||
for i in range(days):
|
||||
d = start_day + timedelta(days=i)
|
||||
r = recover_income_stats_for_date(d, dry_run=dry_run)
|
||||
self.stdout.write(f'=== {r["date"]} ===')
|
||||
self.stdout.write(
|
||||
f' 错乱前日统计: {r["before_daily_amount"]:.2f} 元 / {r["before_daily_count"]} 笔'
|
||||
)
|
||||
self.stdout.write(
|
||||
f' 当日幂等日志: {r["before_log_amount"]:.2f} 元 / {r["before_log_count"]} 笔'
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f' 重建后(正确值): {r["after_amount"]:.2f} 元 / {r["after_count"]} 笔'
|
||||
))
|
||||
if r['items']:
|
||||
self.stdout.write(' 明细:')
|
||||
for ref, cid, amt, kind in r['items']:
|
||||
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
|
||||
|
||||
if not dry_run and options['rebuild_all_daily']:
|
||||
n = rebuild_all_daily_income_from_logs()
|
||||
self.stdout.write(self.style.SUCCESS(f'已按日志全量重写 daily_income_stat: {n} 行'))
|
||||
|
||||
if not dry_run:
|
||||
agg = DailyIncomeStat.objects.filter(date=end_day).aggregate(
|
||||
amt=Sum('total_amount'),
|
||||
cnt=Sum('total_count'),
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\n数据库今日日统计现为: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)} 笔'
|
||||
))
|
||||
self.stdout.write(self.style.SUCCESS('恢复完成,请刷新后台财务页。'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'\n预览完成。重建后金额对的话执行:\n'
|
||||
' python manage.py recover_wechat_income_stats --yes\n'
|
||||
'累计总收入仍不对再加:\n'
|
||||
' python manage.py recover_wechat_income_stats --yes --rebuild-all-daily'
|
||||
))
|
||||
15
config/management/commands/repair_wechat_income.py
Normal file
15
config/management/commands/repair_wechat_income.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""【已废弃】请用 sync_daily_income_from_log,本命令曾导致重复记账。"""
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '已废弃:勿使用。请改用 python manage.py sync_daily_income_from_log --reset-day'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stderr.write(self.style.ERROR(
|
||||
'repair_wechat_income 已禁用(会对已入账订单重复加钱)。\n'
|
||||
'纠正今日虚高请执行:\n'
|
||||
' python manage.py sync_daily_income_from_log --reset-day\n'
|
||||
'可选同时重算累计:\n'
|
||||
' python manage.py sync_daily_income_from_log --reset-day --sync-szjilu'
|
||||
))
|
||||
48
config/management/commands/repopulate_today_income.py
Normal file
48
config/management/commands/repopulate_today_income.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""只恢复今日收入(修复后台四个板子显示 0)。"""
|
||||
from datetime import date
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Sum
|
||||
|
||||
from config.models import DailyIncomeStat
|
||||
from jituan.services.szjilu_accounting import repopulate_today_income_stats
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '重建今日 daily_income_stat(修复财务页今日收入为 0)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--date', type=str, default='', help='日期 YYYY-MM-DD,默认今天')
|
||||
parser.add_argument('--yes', action='store_true', help='确认执行')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
stat_date = date.fromisoformat(options['date']) if options['date'] else date.today()
|
||||
dry_run = not options['yes']
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('【预览】\n'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING('【执行】重建今日收入...\n'))
|
||||
|
||||
r = repopulate_today_income_stats(stat_date, dry_run=dry_run)
|
||||
self.stdout.write(f'日期: {r["date"]}')
|
||||
self.stdout.write(f' 当前: {r["before_amount"]:.2f} 元 / {r["before_count"]} 笔')
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f' 重建后: {r["after_amount"]:.2f} 元 / {r["after_count"]} 笔'
|
||||
))
|
||||
if r['items']:
|
||||
for ref, cid, amt, kind in r['items']:
|
||||
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
|
||||
|
||||
if not dry_run:
|
||||
agg = DailyIncomeStat.objects.filter(date=stat_date).aggregate(
|
||||
amt=Sum('total_amount'), cnt=Sum('total_count'),
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\n完成。今日日统计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)} 笔'
|
||||
))
|
||||
self.stdout.write('请刷新后台财务页。')
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'\n确认后: python manage.py repopulate_today_income --yes'
|
||||
))
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
从订单/充值表恢复 daily_income_stat(不删订单,只重算统计表)。
|
||||
repair/rebuild 搞乱后用这个,不要用 repair_wechat_income。
|
||||
|
||||
预览:python manage.py restore_daily_income_from_orders
|
||||
执行:python manage.py restore_daily_income_from_orders --yes
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from jituan.services.szjilu_accounting import restore_daily_income_from_orders
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '从订单/充值表恢复日收入统计(订单不动)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--since-year', type=int, default=0)
|
||||
parser.add_argument('--until-year', type=int, default=0)
|
||||
parser.add_argument('--yes', action='store_true')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
until_year = options['until_year'] or date.today().year
|
||||
since_year = options['since_year'] or (until_year - 1)
|
||||
dry_run = not options['yes']
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('【预览】订单数据不会动\n'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING('【执行】按订单/充值重算日统计...\n'))
|
||||
|
||||
r = restore_daily_income_from_orders(since_year, until_year, dry_run=dry_run)
|
||||
self.stdout.write(f'范围: {r["since_year"]} ~ {r["until_year"]}')
|
||||
self.stdout.write(f' 支付笔数(订单表): {r["payment_rows"]}')
|
||||
self.stdout.write(f' 日统计行数: {r["daily_rows"]}')
|
||||
self.stdout.write(f' 当前 daily 合计: {r["before_daily_total"]:.2f} 元')
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f' 恢复后 daily 合计: {r["after_daily_total"]:.2f} 元'
|
||||
))
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'\n确认: python manage.py restore_daily_income_from_orders --yes'
|
||||
))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS('\n完成。推 kefu 后刷新财务页。'))
|
||||
68
config/management/commands/restore_finance_stats.py
Normal file
68
config/management/commands/restore_finance_stats.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
恢复 daily_income_stat(每日收入统计表)全表数据。
|
||||
|
||||
repair_wechat_income 对该表的破坏:
|
||||
- 把扫描到的每笔订单/充值重复写入 daily_income_stat
|
||||
- 金额记到「执行命令那天」,不是真实支付日
|
||||
- 含退款单(Status=5)也被当成收入加进去
|
||||
|
||||
本命令按 platform_income_log + 无日志的已支付单,重建整张 daily_income_stat。
|
||||
不碰 daily_payout_stat,不碰 szjilu,不删订单。
|
||||
|
||||
预览:python manage.py restore_finance_stats
|
||||
执行:python manage.py restore_finance_stats --yes
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Sum
|
||||
|
||||
from config.models import DailyIncomeStat, DailyPayoutStat
|
||||
from jituan.services.szjilu_accounting import rebuild_daily_income_stat_table
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '恢复 daily_income_stat 全表(财务四个板子读这张表)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--yes', action='store_true', help='确认写入数据库')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = not options['yes']
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('【预览】未改数据库\n'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING('【执行】重建 daily_income_stat...\n'))
|
||||
|
||||
r = rebuild_daily_income_stat_table(dry_run=dry_run)
|
||||
|
||||
self.stdout.write(f' 支付笔数(去重): {r["payment_rows"]}')
|
||||
self.stdout.write(f' 其中日志: {r["from_log"]}')
|
||||
self.stdout.write(f' 补漏订单: {r["gap_orders"]}')
|
||||
self.stdout.write(f' 补漏充值: {r["gap_cz"]}')
|
||||
self.stdout.write(f' 日统计行数: {r["daily_rows"]}')
|
||||
self.stdout.write(f' 恢复前 daily 合计: {r["before_daily_total"]:.2f} 元')
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f' 恢复后 daily 合计: {r["after_daily_total"]:.2f} 元'
|
||||
))
|
||||
self.stdout.write(f' 其中今日收入: {r["today_amount"]:.2f} 元 / {r["today_count"]} 笔')
|
||||
|
||||
payout_today = DailyPayoutStat.objects.filter(date=date.today()).aggregate(
|
||||
t=Sum('total_amount'),
|
||||
)['t'] or 0
|
||||
self.stdout.write(
|
||||
f' (未动)今日支出表: {float(payout_today):.2f} 元'
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'\n预览数字合理后执行:\n'
|
||||
' python manage.py restore_finance_stats --yes\n'
|
||||
'然后 push 代码并刷新财务页。'
|
||||
))
|
||||
else:
|
||||
check = DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\n完成。数据库 daily_income_stat 合计: {float(check):.2f} 元'
|
||||
))
|
||||
68
config/management/commands/sync_daily_income_from_log.py
Normal file
68
config/management/commands/sync_daily_income_from_log.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
用 platform_income_log 重写 daily_income_stat,纠正重复记账后的虚高数字。
|
||||
权威数据源是 platform_income_log(每笔支付一条,biz_ref 唯一),不是扫订单重算。
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from jituan.services.szjilu_accounting import (
|
||||
sync_daily_income_stat_for_date,
|
||||
sync_szjilu_income_from_logs,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
'按 platform_income_log 同步 daily_income_stat(纠正今日收入虚高/笔数不准)。'
|
||||
'不要用 repair_wechat_income,那个会重复加钱。'
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--date',
|
||||
type=str,
|
||||
default='',
|
||||
help='同步日期 YYYY-MM-DD,默认今天',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--days',
|
||||
type=int,
|
||||
default=1,
|
||||
help='从指定日期起向前同步几天,默认 1',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--reset-day',
|
||||
action='store_true',
|
||||
help='同步前先删除该日全部日统计行,再按日志重建(纠正重复记账必选)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--sync-szjilu',
|
||||
action='store_true',
|
||||
help='同时按日志全量重算 szjilu 累计收入/今日流水',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['date']:
|
||||
end_day = date.fromisoformat(options['date'])
|
||||
else:
|
||||
end_day = date.today()
|
||||
days = max(1, int(options['days'] or 1))
|
||||
start_day = end_day - timedelta(days=days - 1)
|
||||
reset_day = bool(options['reset_day'])
|
||||
|
||||
total = 0
|
||||
for i in range(days):
|
||||
d = start_day + timedelta(days=i)
|
||||
n = sync_daily_income_stat_for_date(d, reset_day=reset_day)
|
||||
total += n
|
||||
self.stdout.write(f'{d}: 已同步 {n} 个俱乐部日统计行')
|
||||
|
||||
if options['sync_szjilu']:
|
||||
sz = sync_szjilu_income_from_logs()
|
||||
self.stdout.write(f'szjilu: 已重算 {sz} 个俱乐部累计收入')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'完成。今日收入/笔数现与 platform_income_log 一致。'
|
||||
f'若仍有漏记支付,请确认 migrate config 后等新支付自动入账。'
|
||||
))
|
||||
133
config/management/commands/undo_repair_wechat_income.py
Normal file
133
config/management/commands/undo_repair_wechat_income.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
【反向撤销】repair_wechat_income 造成的重复记账。
|
||||
|
||||
你当时执行的指令是:
|
||||
python manage.py repair_wechat_income
|
||||
或:
|
||||
python manage.py repair_wechat_income --date 2026-06-24 --days 7
|
||||
|
||||
repair 逻辑:按「当日创建的已支付订单/充值单」逐条 +金额、+笔数、写 platform_income_log、加 szjilu。
|
||||
本命令用完全相同扫描列表,逐条反向 -金额、-笔数、删日志、减 szjilu。
|
||||
|
||||
用法:
|
||||
1. 预览(不改库):
|
||||
python manage.py undo_repair_wechat_income
|
||||
|
||||
2. 确认后撤销(加多少减多少):
|
||||
python manage.py undo_repair_wechat_income --yes
|
||||
|
||||
3. 若当时带了参数,必须一致:
|
||||
python manage.py undo_repair_wechat_income --date 2026-06-01 --days 30 --run-date 2026-06-24 --yes
|
||||
(--run-date = 你执行 repair 那天,默认今天)
|
||||
|
||||
4. 全部清零(恢复到 0):
|
||||
python manage.py undo_repair_wechat_income --zero-all --yes
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Sum
|
||||
|
||||
from config.models import DailyIncomeStat
|
||||
from jituan.services.szjilu_accounting import (
|
||||
undo_repair_wechat_income_period,
|
||||
zero_all_income_statistics,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '反向撤销 repair_wechat_income 造成的收支重复记账'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--date', type=str, default='',
|
||||
help='与 repair 相同的 --date(扫描区间结束日),默认今天',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--days', type=int, default=1,
|
||||
help='与 repair 相同的 --days,默认 1',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--run-date', type=str, default='',
|
||||
help='你执行 repair 的日期(金额被记到哪天的 daily),默认今天',
|
||||
)
|
||||
parser.add_argument('--yes', action='store_true', help='确认执行')
|
||||
parser.add_argument(
|
||||
'--zero-all', action='store_true',
|
||||
help='收支统计全部清零(日收入、日志、szjilu 收入流水归零)',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['zero_all']:
|
||||
self._zero_all(not options['yes'])
|
||||
return
|
||||
|
||||
if options['date']:
|
||||
end_day = date.fromisoformat(options['date'])
|
||||
else:
|
||||
end_day = date.today()
|
||||
days = max(1, int(options['days'] or 1))
|
||||
start_day = end_day - timedelta(days=days - 1)
|
||||
if options['run_date']:
|
||||
inflation_date = date.fromisoformat(options['run_date'])
|
||||
else:
|
||||
inflation_date = date.today()
|
||||
|
||||
dry_run = not options['yes']
|
||||
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'========== 反向撤销 repair_wechat_income ==========\n'
|
||||
f'原 repair 扫描区间: {start_day} ~ {end_day} (--days {days})\n'
|
||||
f'金额被记到日统计日期: {inflation_date} (--run-date)\n'
|
||||
))
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write('【预览】未改数据库\n')
|
||||
else:
|
||||
self.stdout.write('【执行】正在反向扣减...\n')
|
||||
|
||||
r = undo_repair_wechat_income_period(
|
||||
start_day, end_day, inflation_date, dry_run=dry_run,
|
||||
)
|
||||
|
||||
self.stdout.write(f' 将撤销条数: {r["undo_count"]} 条')
|
||||
self.stdout.write(f' 将扣减金额: {r["undo_amount"]:.2f} 元')
|
||||
self.stdout.write(
|
||||
f' 执行前 {inflation_date} 日统计: {r["before_daily_on_inflation_date"]:.2f} 元'
|
||||
)
|
||||
|
||||
if r['items']:
|
||||
self.stdout.write(' 明细:')
|
||||
for ref, cid, amt, kind in r['items']:
|
||||
self.stdout.write(f' - {ref} {amt}元 club={cid} ({kind})')
|
||||
|
||||
if not dry_run:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\n执行后 {inflation_date} 日统计: {r.get("after_daily_on_inflation_date", 0):.2f} 元'
|
||||
))
|
||||
self.stdout.write(self.style.SUCCESS('反向撤销完成,请刷新后台财务页。'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'\n预览完成。确认后执行(参数与 repair 保持一致):\n'
|
||||
f' python manage.py undo_repair_wechat_income --date {end_day} --days {days} '
|
||||
f'--run-date {inflation_date} --yes\n'
|
||||
'全部清零:\n'
|
||||
' python manage.py undo_repair_wechat_income --zero-all --yes'
|
||||
))
|
||||
|
||||
def _zero_all(self, dry_run):
|
||||
agg = DailyIncomeStat.objects.aggregate(amt=Sum('total_amount'), cnt=Sum('total_count'))
|
||||
self.stdout.write(self.style.ERROR(
|
||||
'========== 收支统计全部清零 ==========\n'
|
||||
f'当前 daily_income_stat 合计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)} 笔\n'
|
||||
'将删除: platform_income_log 全部、daily_income_stat 全部\n'
|
||||
'将归零: szjilu.TotalIncome / TotalFlow / DailyFlow\n'
|
||||
))
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'预览。确认清零:\n'
|
||||
' python manage.py undo_repair_wechat_income --zero-all --yes'
|
||||
))
|
||||
return
|
||||
zero_all_income_statistics()
|
||||
self.stdout.write(self.style.SUCCESS('已全部清零。请刷新后台。'))
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-14 19:59
|
||||
# Generated by Django 4.2.27 on 2026-06-17 21:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -16,8 +17,8 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('leixing', models.IntegerField(blank=True, null=True, verbose_name='公告类型')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||
('jieshao', models.TextField(blank=True, null=True, verbose_name='公告介绍')),
|
||||
],
|
||||
options={
|
||||
@@ -32,8 +33,8 @@ class Migration(migrations.Migration):
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('tupian_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='轮播图片URL')),
|
||||
('leixing', models.IntegerField(blank=True, null=True, verbose_name='图片类型:1:轮播2:个人中心背景')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '轮播',
|
||||
@@ -41,13 +42,57 @@ class Migration(migrations.Migration):
|
||||
'db_table': 'lunbo',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PopupConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('popup_id', models.CharField(max_length=50, verbose_name='弹窗唯一ID')),
|
||||
('title', models.CharField(blank=True, max_length=200, verbose_name='标题(后台用)')),
|
||||
('description', models.TextField(blank=True, verbose_name='描述(后台用)')),
|
||||
('strategy_type', models.CharField(choices=[('once', '仅一次'), ('daily', '每日'), ('weekly', '每周'), ('monthly', '每月'), ('yearly', '每年'), ('always', '始终'), ('count', '周期内次数限制')], default='once', max_length=20)),
|
||||
('max_count', models.PositiveIntegerField(default=1, verbose_name='周期内最大展示次数')),
|
||||
('reset_interval', models.CharField(blank=True, choices=[('day', '天'), ('week', '周'), ('month', '月'), ('year', '年')], max_length=20, verbose_name='重置周期(count类型时使用)')),
|
||||
('duration', models.PositiveIntegerField(default=0, verbose_name='强制阅读秒数(0=无强制)')),
|
||||
('force_even_muted', models.BooleanField(default=False, verbose_name='强制弹出(忽略用户今日屏蔽)')),
|
||||
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('start_time', models.DateTimeField(blank=True, null=True, verbose_name='生效开始时间')),
|
||||
('end_time', models.DateTimeField(blank=True, null=True, verbose_name='生效结束时间')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '弹窗配置',
|
||||
'verbose_name_plural': '弹窗配置',
|
||||
'db_table': 'popup_config',
|
||||
'ordering': ['sort_order', 'id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PopupPage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('page_key', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='页面标识')),
|
||||
('name', models.CharField(max_length=100, verbose_name='页面名称')),
|
||||
('description', models.TextField(blank=True, verbose_name='描述')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('ignore_user_mute', models.BooleanField(default=False, verbose_name='忽略用户今日屏蔽')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '弹窗页面',
|
||||
'verbose_name_plural': '弹窗页面',
|
||||
'db_table': 'popup_page',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Qunpeizhi',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('peizhiid', models.IntegerField(blank=True, null=True, verbose_name='群类型:1.打手 2.管事')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||
('neirong', models.CharField(blank=True, max_length=200, null=True, verbose_name='群介绍')),
|
||||
('qunid', models.CharField(blank=True, max_length=100, null=True, verbose_name='群id')),
|
||||
('jieshao', models.CharField(blank=True, max_length=200, null=True, verbose_name='介绍')),
|
||||
@@ -65,8 +110,8 @@ class Migration(migrations.Migration):
|
||||
('zongzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总支出')),
|
||||
('jrzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日支出')),
|
||||
('jrls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日流水')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '收支记录',
|
||||
@@ -74,14 +119,30 @@ class Migration(migrations.Migration):
|
||||
'db_table': 'szjilu',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TixianQuotaDefault',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('leixing', models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], unique=True, verbose_name='用户类型')),
|
||||
('default_quota', models.DecimalField(decimal_places=2, default=20.0, max_digits=10, verbose_name='默认每日提现限额(元)')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '默认提现限额',
|
||||
'verbose_name_plural': '默认提现限额',
|
||||
'db_table': 'tixian_quota_default',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tupianpeizhi',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('tupian', models.CharField(blank=True, max_length=500, null=True, verbose_name='配置图片URL')),
|
||||
('leixing', models.PositiveIntegerField(blank=True, null=True, verbose_name='提现类型1:打手规则图片2:默认头像图片')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||
('fangwen_cishu', models.PositiveIntegerField(default=0, verbose_name='访问次数')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '配置图片',
|
||||
@@ -89,4 +150,147 @@ class Migration(migrations.Migration):
|
||||
'db_table': 'tupianpeizhi',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WithdrawConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mode', models.IntegerField(choices=[(1, '自动提现'), (2, '手动提现')], default=2, verbose_name='提现模式')),
|
||||
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '提现模式配置',
|
||||
'verbose_name_plural': '提现模式配置',
|
||||
'db_table': 'withdraw_config',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ShangjiaMoban',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='模板ID')),
|
||||
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
|
||||
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
|
||||
('moban_jieshao', models.CharField(max_length=500, verbose_name='模板介绍')),
|
||||
('jiage', models.DecimalField(decimal_places=2, help_text='单位:元', max_digits=10, verbose_name='模板价格')),
|
||||
('fabu_shuliang', models.PositiveIntegerField(default=0, verbose_name='发布数量')),
|
||||
('yongjin_qiangdan', models.BooleanField(default=False, verbose_name='是否开启佣金抢单')),
|
||||
('yongjin_jiage', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='佣金价格')),
|
||||
('chenghao_id', models.IntegerField(blank=True, null=True, verbose_name='关联称号ID')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '商家订单模板',
|
||||
'verbose_name_plural': '商家订单模板',
|
||||
'db_table': 'shangjia_moban',
|
||||
'indexes': [models.Index(fields=['yonghu_id', 'leixing_id'], name='shangjia_mo_yonghu__bb287d_idx'), models.Index(fields=['yonghu_id', 'leixing_id', 'id'], name='shangjia_mo_yonghu__4240d0_idx')],
|
||||
'unique_together': {('yonghu_id', 'leixing_id', 'moban_jieshao')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ShangjiaLianjie',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='链接ID')),
|
||||
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
|
||||
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
|
||||
('moban_id', models.PositiveIntegerField(db_index=True, verbose_name='模板ID')),
|
||||
('lianjie', models.CharField(max_length=500, unique=True, verbose_name='链接地址')),
|
||||
('dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='订单ID')),
|
||||
('is_used', models.BooleanField(db_index=True, default=False, verbose_name='是否已使用')),
|
||||
('shiyongzhe_ip', models.CharField(blank=True, max_length=50, null=True, verbose_name='使用者IP')),
|
||||
('youxiao_shijian', models.DateTimeField(blank=True, null=True, verbose_name='有效截止时间')),
|
||||
('lianjie_token', models.CharField(blank=True, db_index=True, max_length=32, null=True, unique=True, verbose_name='链接Token')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('previous_lianjie_id', models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='上一个链接ID')),
|
||||
('previous_dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='上一个订单ID')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '商家链接',
|
||||
'verbose_name_plural': '商家链接',
|
||||
'db_table': 'shangjia_lianjie',
|
||||
'indexes': [models.Index(fields=['yonghu_id', 'moban_id', 'leixing_id'], name='shangjia_li_yonghu__9f2b25_idx'), models.Index(fields=['lianjie_token'], name='shangjia_li_lianjie_99014c_idx'), models.Index(fields=['dingdan_id'], name='shangjia_li_dingdan_95b682_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PopupImage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('image_url', models.CharField(max_length=500, verbose_name='图片相对路径')),
|
||||
('text', models.TextField(blank=True, verbose_name='图片上方文字')),
|
||||
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('popup_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='config.popupconfig')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '弹窗图片',
|
||||
'verbose_name_plural': '弹窗图片',
|
||||
'db_table': 'popup_image',
|
||||
'ordering': ['sort_order', 'id'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='popupconfig',
|
||||
name='page',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='popups', to='config.popuppage'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DailyPayoutStat',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
|
||||
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
|
||||
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
|
||||
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
|
||||
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='出款总额')),
|
||||
('total_count', models.PositiveIntegerField(default=0, verbose_name='出款总笔数')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '每日出款统计',
|
||||
'verbose_name_plural': '每日出款统计',
|
||||
'db_table': 'daily_payout_stat',
|
||||
'indexes': [models.Index(fields=['date'], name='daily_payou_date_b534ec_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_payou_year_6376ca_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DailyIncomeStat',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
|
||||
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
|
||||
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
|
||||
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
|
||||
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='总收入金额')),
|
||||
('total_count', models.PositiveIntegerField(default=0, verbose_name='总收入笔数')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '每日收入统计',
|
||||
'verbose_name_plural': '每日收入统计',
|
||||
'db_table': 'daily_income_stat',
|
||||
'indexes': [models.Index(fields=['date'], name='daily_incom_date_75e6df_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_incom_year_d85d10_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AccountPermission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')),
|
||||
('permission_code', models.IntegerField(verbose_name='权限代码')),
|
||||
('is_enabled', models.BooleanField(default=False, verbose_name='是否开启')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '账号权限',
|
||||
'verbose_name_plural': '账号权限',
|
||||
'db_table': 'account_permission',
|
||||
'unique_together': {('account_id', 'permission_code')},
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='popupconfig',
|
||||
unique_together={('page', 'popup_id')},
|
||||
),
|
||||
]
|
||||
|
||||
64
config/migrations/0002_rename_pinyin_to_pascal.py
Normal file
64
config/migrations/0002_rename_pinyin_to_pascal.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# Generated manually to rename pinyin fields to PascalCase
|
||||
# db_column is preserved, so these are no-ops at the database level
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Lunbo
|
||||
migrations.RenameField(model_name='lunbo', old_name='tupian_url', new_name='ImageURL'),
|
||||
migrations.RenameField(model_name='lunbo', old_name='leixing', new_name='ImageType'),
|
||||
|
||||
# Gonggao
|
||||
migrations.RenameField(model_name='gonggao', old_name='leixing', new_name='NoticeType'),
|
||||
migrations.RenameField(model_name='gonggao', old_name='jieshao', new_name='Content'),
|
||||
|
||||
# Qunpeizhi
|
||||
migrations.RenameField(model_name='qunpeizhi', old_name='peizhiid', new_name='GroupType'),
|
||||
migrations.RenameField(model_name='qunpeizhi', old_name='neirong', new_name='GroupContent'),
|
||||
migrations.RenameField(model_name='qunpeizhi', old_name='qunid', new_name='GroupID'),
|
||||
migrations.RenameField(model_name='qunpeizhi', old_name='jieshao', new_name='Description'),
|
||||
|
||||
# Tupianpeizhi
|
||||
migrations.RenameField(model_name='tupianpeizhi', old_name='tupian', new_name='ImageURL'),
|
||||
migrations.RenameField(model_name='tupianpeizhi', old_name='leixing', new_name='ImageType'),
|
||||
migrations.RenameField(model_name='tupianpeizhi', old_name='fangwen_cishu', new_name='AccessCount'),
|
||||
|
||||
# Szjilu
|
||||
migrations.RenameField(model_name='szjilu', old_name='zongsy', new_name='TotalIncome'),
|
||||
migrations.RenameField(model_name='szjilu', old_name='zongls', new_name='TotalFlow'),
|
||||
migrations.RenameField(model_name='szjilu', old_name='zongzc', new_name='TotalExpense'),
|
||||
migrations.RenameField(model_name='szjilu', old_name='jrzc', new_name='DailyExpense'),
|
||||
migrations.RenameField(model_name='szjilu', old_name='jrls', new_name='DailyFlow'),
|
||||
|
||||
# ShangjiaMoban
|
||||
migrations.RenameField(model_name='shangjiamoban', old_name='yonghu_id', new_name='UserID'),
|
||||
migrations.RenameField(model_name='shangjiamoban', old_name='leixing_id', new_name='ProductTypeID'),
|
||||
migrations.RenameField(model_name='shangjiamoban', old_name='moban_jieshao', new_name='TemplateDesc'),
|
||||
migrations.RenameField(model_name='shangjiamoban', old_name='jiage', new_name='Price'),
|
||||
migrations.RenameField(model_name='shangjiamoban', old_name='fabu_shuliang', new_name='PublishedCount'),
|
||||
migrations.RenameField(model_name='shangjiamoban', old_name='yongjin_qiangdan', new_name='CommissionGrabEnabled'),
|
||||
migrations.RenameField(model_name='shangjiamoban', old_name='yongjin_jiage', new_name='CommissionPrice'),
|
||||
migrations.RenameField(model_name='shangjiamoban', old_name='chenghao_id', new_name='TitleID'),
|
||||
|
||||
# ShangjiaLianjie
|
||||
migrations.RenameField(model_name='shangjialianjie', old_name='yonghu_id', new_name='UserID'),
|
||||
migrations.RenameField(model_name='shangjialianjie', old_name='leixing_id', new_name='ProductTypeID'),
|
||||
migrations.RenameField(model_name='shangjialianjie', old_name='moban_id', new_name='TemplateID'),
|
||||
migrations.RenameField(model_name='shangjialianjie', old_name='lianjie', new_name='LinkURL'),
|
||||
migrations.RenameField(model_name='shangjialianjie', old_name='dingdan_id', new_name='OrderID'),
|
||||
migrations.RenameField(model_name='shangjialianjie', old_name='shiyongzhe_ip', new_name='UserIP'),
|
||||
migrations.RenameField(model_name='shangjialianjie', old_name='youxiao_shijian', new_name='ExpireTime'),
|
||||
migrations.RenameField(model_name='shangjialianjie', old_name='lianjie_token', new_name='LinkToken'),
|
||||
migrations.RenameField(model_name='shangjialianjie', old_name='previous_lianjie_id', new_name='PreviousLinkID'),
|
||||
migrations.RenameField(model_name='shangjialianjie', old_name='previous_dingdan_id', new_name='PreviousOrderID'),
|
||||
|
||||
# TixianQuotaDefault
|
||||
migrations.RenameField(model_name='tixianquotadefault', old_name='leixing', new_name='UserType'),
|
||||
]
|
||||
@@ -1,56 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-01-27 12:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ShangjiaMoban',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='模板ID')),
|
||||
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
|
||||
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
|
||||
('moban_jieshao', models.CharField(max_length=500, verbose_name='模板介绍')),
|
||||
('jiage', models.DecimalField(decimal_places=2, help_text='单位:元', max_digits=10, verbose_name='模板价格')),
|
||||
('fabu_shuliang', models.PositiveIntegerField(default=0, 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': 'shangjia_moban',
|
||||
'indexes': [models.Index(fields=['yonghu_id', 'leixing_id'], name='shangjia_mo_yonghu__bb287d_idx'), models.Index(fields=['yonghu_id', 'leixing_id', 'id'], name='shangjia_mo_yonghu__4240d0_idx')],
|
||||
'unique_together': {('yonghu_id', 'leixing_id', 'moban_jieshao')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ShangjiaLianjie',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='链接ID')),
|
||||
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
|
||||
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
|
||||
('moban_id', models.PositiveIntegerField(db_index=True, verbose_name='模板ID')),
|
||||
('lianjie', models.CharField(max_length=500, unique=True, verbose_name='链接地址')),
|
||||
('dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='订单ID')),
|
||||
('is_used', models.BooleanField(db_index=True, default=False, verbose_name='是否已使用')),
|
||||
('shiyongzhe_ip', models.CharField(blank=True, max_length=50, null=True, verbose_name='使用者IP')),
|
||||
('youxiao_shijian', models.DateTimeField(blank=True, null=True, verbose_name='有效截止时间')),
|
||||
('lianjie_token', models.CharField(blank=True, db_index=True, max_length=32, null=True, unique=True, verbose_name='链接Token')),
|
||||
('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': 'shangjia_lianjie',
|
||||
'indexes': [models.Index(fields=['yonghu_id', 'moban_id', 'leixing_id'], name='shangjia_li_yonghu__9f2b25_idx'), models.Index(fields=['lianjie_token'], name='shangjia_li_lianjie_99014c_idx'), models.Index(fields=['dingdan_id'], name='shangjia_li_dingdan_95b682_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,76 @@
|
||||
# Generated by Django 4.2.27 on 2026-06-19 11:36
|
||||
# Modified to use SeparateDatabaseAndState for RemoveIndex to bypass
|
||||
# MySQL backend's _create_missing_fk_index check that fails after RenameField
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0002_rename_pinyin_to_pascal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# RemoveIndex operations wrapped in SeparateDatabaseAndState
|
||||
# to bypass MySQL backend's FK index check that references old field names
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.RemoveIndex(
|
||||
model_name='shangjialianjie',
|
||||
name='shangjia_li_yonghu__9f2b25_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='shangjialianjie',
|
||||
name='shangjia_li_lianjie_99014c_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='shangjialianjie',
|
||||
name='shangjia_li_dingdan_95b682_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='shangjiamoban',
|
||||
name='shangjia_mo_yonghu__bb287d_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='shangjiamoban',
|
||||
name='shangjia_mo_yonghu__4240d0_idx',
|
||||
),
|
||||
],
|
||||
database_operations=[
|
||||
migrations.RunSQL(
|
||||
sql=[
|
||||
'DROP INDEX `shangjia_li_yonghu__9f2b25_idx` ON `shangjia_lianjie`',
|
||||
'DROP INDEX `shangjia_li_lianjie_99014c_idx` ON `shangjia_lianjie`',
|
||||
'DROP INDEX `shangjia_li_dingdan_95b682_idx` ON `shangjia_lianjie`',
|
||||
'DROP INDEX `shangjia_mo_yonghu__bb287d_idx` ON `shangjia_moban`',
|
||||
'DROP INDEX `shangjia_mo_yonghu__4240d0_idx` ON `shangjia_moban`',
|
||||
],
|
||||
reverse_sql=[
|
||||
# No reverse needed - indexes will be recreated by AddIndex
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
# AddIndex operations - these should work fine since fields exist with new names
|
||||
migrations.AddIndex(
|
||||
model_name='shangjialianjie',
|
||||
index=models.Index(fields=['UserID', 'TemplateID', 'ProductTypeID'], name='shangjia_li_UserID_e59d94_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='shangjialianjie',
|
||||
index=models.Index(fields=['LinkToken'], name='shangjia_li_LinkTok_6b32bb_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='shangjialianjie',
|
||||
index=models.Index(fields=['OrderID'], name='shangjia_li_OrderID_e7ddf9_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='shangjiamoban',
|
||||
index=models.Index(fields=['UserID', 'ProductTypeID'], name='shangjia_mo_UserID_abd699_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='shangjiamoban',
|
||||
index=models.Index(fields=['UserID', 'ProductTypeID', 'id'], name='shangjia_mo_UserID_2dcb54_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-03-16 17:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0002_shangjiamoban_shangjialianjie'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tupianpeizhi',
|
||||
name='fangwen_cishu',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='访问次数'),
|
||||
),
|
||||
]
|
||||
47
config/migrations/0004_daily_stat_club_id.py
Normal file
47
config/migrations/0004_daily_stat_club_id.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0003_remove_shangjialianjie_shangjia_li_yonghu__9f2b25_idx_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dailyincomestat',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='dailypayoutstat',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dailyincomestat',
|
||||
name='date',
|
||||
field=models.DateField(db_index=True, verbose_name='统计日期'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dailypayoutstat',
|
||||
name='date',
|
||||
field=models.DateField(db_index=True, verbose_name='统计日期'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='dailyincomestat',
|
||||
index=models.Index(fields=['club_id', 'date'], name='idx_income_club_date'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='dailypayoutstat',
|
||||
index=models.Index(fields=['club_id', 'date'], name='idx_payout_club_date'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='dailyincomestat',
|
||||
constraint=models.UniqueConstraint(fields=('date', 'club_id'), name='uniq_daily_income_club_date'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='dailypayoutstat',
|
||||
constraint=models.UniqueConstraint(fields=('date', 'club_id'), name='uniq_daily_payout_club_date'),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-03-20 15:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0003_tupianpeizhi_fangwen_cishu'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TixianQuotaDefault',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('leixing', models.IntegerField(choices=[(1, '打手'), (2, '管事')], unique=True, verbose_name='用户类型')),
|
||||
('default_quota', models.DecimalField(decimal_places=2, default=20.0, max_digits=10, 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': 'tixian_quota_default',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,76 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-03-24 17:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0004_tixianquotadefault'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrderTypeMapping',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('partner_club_id', models.CharField(db_index=True, max_length=6, verbose_name='对方俱乐部ID')),
|
||||
('partner_type_id', models.PositiveIntegerField(verbose_name='对方订单类型ID')),
|
||||
('partner_type_name', models.CharField(max_length=100, verbose_name='对方订单类型名称')),
|
||||
('our_type_id', models.PositiveIntegerField(verbose_name='我方订单类型ID')),
|
||||
('our_type_name', models.CharField(max_length=100, 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': 'order_type_mapping',
|
||||
'unique_together': {('partner_club_id', 'partner_type_id')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DailyDispatchStat',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(db_index=True, verbose_name='统计日期')),
|
||||
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
|
||||
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
|
||||
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
|
||||
('direction', models.PositiveSmallIntegerField(choices=[(1, '我方派单'), (2, '对方派单')], verbose_name='派单方向')),
|
||||
('partner_club_id', models.CharField(db_index=True, max_length=6, verbose_name='对方俱乐部ID')),
|
||||
('dispatch_count', models.PositiveIntegerField(default=0, verbose_name='派单数')),
|
||||
('dispatch_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='派单总额')),
|
||||
('success_count', models.PositiveIntegerField(default=0, verbose_name='成交订单数')),
|
||||
('success_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, 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': 'daily_dispatch_stat',
|
||||
'indexes': [models.Index(fields=['year', 'month', 'day', 'direction', 'partner_club_id'], name='daily_dispa_year_f98f91_idx'), models.Index(fields=['date'], name='daily_dispa_date_9ac7fe_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Club',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(db_index=True, max_length=6, verbose_name='本俱乐部ID')),
|
||||
('partner_club_id', models.CharField(max_length=6, unique=True, verbose_name='对方俱乐部ID')),
|
||||
('is_interop_enabled', models.BooleanField(default=False, verbose_name='是否开启平台互通')),
|
||||
('is_receive_enabled', models.BooleanField(default=False, verbose_name='是否同意接收对方订单')),
|
||||
('dashou_rate', models.DecimalField(decimal_places=4, default=1.0, max_digits=5, verbose_name='打手分红费率')),
|
||||
('partner_domain', models.URLField(blank=True, 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': 'club',
|
||||
'indexes': [models.Index(fields=['club_id'], name='club_club_id_e75636_idx'), models.Index(fields=['partner_club_id'], name='club_partner_b76039_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
37
config/migrations/0005_szjilu_club_id.py
Normal file
37
config/migrations/0005_szjilu_club_id.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def backfill_szjilu_club_id(apps, schema_editor):
|
||||
Szjilu = apps.get_model('config', 'Szjilu')
|
||||
for row in Szjilu.objects.all():
|
||||
if not getattr(row, 'club_id', None):
|
||||
row.club_id = 'xq'
|
||||
row.save(update_fields=['club_id'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0004_daily_stat_club_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='szjilu',
|
||||
name='club_id',
|
||||
field=models.CharField(
|
||||
db_index=True,
|
||||
default='xq',
|
||||
max_length=16,
|
||||
verbose_name='俱乐部ID',
|
||||
),
|
||||
),
|
||||
migrations.RunPython(backfill_szjilu_club_id, migrations.RunPython.noop),
|
||||
migrations.AddConstraint(
|
||||
model_name='szjilu',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('club_id',),
|
||||
name='uniq_szjilu_club_id',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,52 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-04-01 09:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0005_ordertypemapping_dailydispatchstat_club'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ClubConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(db_index=True, max_length=6, verbose_name='俱乐部ID')),
|
||||
('club_nickname', models.CharField(blank=True, default='', max_length=50, verbose_name='俱乐部昵称')),
|
||||
('club_avatar', models.CharField(blank=True, default='', max_length=500, verbose_name='俱乐部头像地址')),
|
||||
('backend_domain', models.CharField(blank=True, default='', max_length=200, verbose_name='后台域名')),
|
||||
('chat_api_key', models.CharField(blank=True, default='', max_length=200, verbose_name='聊天API密钥')),
|
||||
('chat_api_id', models.CharField(blank=True, default='', max_length=200, verbose_name='聊天API ID')),
|
||||
('customer_service_api_url', models.CharField(blank=True, default='', max_length=200, verbose_name='客服API URL')),
|
||||
('customer_service_corp_id', models.CharField(blank=True, default='', max_length=100, verbose_name='客服企业ID')),
|
||||
('storage_bucket_domain', models.CharField(blank=True, default='', max_length=500, verbose_name='存储桶域名')),
|
||||
('is_self', models.SmallIntegerField(choices=[(0, '对方'), (1, '我方')], db_index=True, default=0, 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': 'club_config',
|
||||
'unique_together': {('club_id',)},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AccountPermission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')),
|
||||
('permission_code', models.IntegerField(verbose_name='权限代码')),
|
||||
('is_enabled', models.BooleanField(default=False, verbose_name='是否开启')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '账号权限',
|
||||
'verbose_name_plural': '账号权限',
|
||||
'db_table': 'account_permission',
|
||||
'unique_together': {('account_id', 'permission_code')},
|
||||
},
|
||||
),
|
||||
]
|
||||
81
config/migrations/0006_display_club_id.py
Normal file
81
config/migrations/0006_display_club_id.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Max
|
||||
|
||||
|
||||
def _dedupe_popuppage(apps, schema_editor):
|
||||
PopupPage = apps.get_model('config', 'PopupPage')
|
||||
for row in PopupPage.objects.values('club_id', 'page_key').annotate(max_id=Max('id')):
|
||||
PopupPage.objects.filter(
|
||||
club_id=row['club_id'],
|
||||
page_key=row['page_key'],
|
||||
).exclude(id=row['max_id']).delete()
|
||||
|
||||
|
||||
def _dedupe_tupianpeizhi(apps, schema_editor):
|
||||
Tupianpeizhi = apps.get_model('config', 'Tupianpeizhi')
|
||||
for row in Tupianpeizhi.objects.values('club_id', 'ImageType').annotate(max_id=Max('id')):
|
||||
Tupianpeizhi.objects.filter(
|
||||
club_id=row['club_id'],
|
||||
ImageType=row['ImageType'],
|
||||
).exclude(id=row['max_id']).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0005_szjilu_club_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='gonggao',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='lunbo',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='lunbo',
|
||||
name='page_key',
|
||||
field=models.CharField(
|
||||
db_index=True, default='order_pool', max_length=32,
|
||||
verbose_name='页面标识(order_pool/accept_order/merchant_home/dashou_center)',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='popuppage',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tupianpeizhi',
|
||||
name='club_id',
|
||||
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='popuppage',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='popuppage',
|
||||
name='page_key',
|
||||
field=models.CharField(db_index=True, max_length=50, verbose_name='页面标识'),
|
||||
),
|
||||
migrations.RunPython(_dedupe_popuppage, migrations.RunPython.noop),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='popuppage',
|
||||
unique_together={('club_id', 'page_key')},
|
||||
),
|
||||
migrations.RunPython(_dedupe_tupianpeizhi, migrations.RunPython.noop),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='tupianpeizhi',
|
||||
unique_together={('club_id', 'ImageType')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='lunbo',
|
||||
index=models.Index(fields=['club_id', 'page_key', 'ImageType'], name='lunbo_club_page_type_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-04-02 13:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0006_clubconfig_accountpermission'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='club',
|
||||
name='receive_rate',
|
||||
field=models.DecimalField(decimal_places=4, default=1.0, max_digits=5, verbose_name='接收打手分红费率'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='dailydispatchstat',
|
||||
name='claimed_amount',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='抢单总额'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='dailydispatchstat',
|
||||
name='claimed_count',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='抢单数'),
|
||||
),
|
||||
]
|
||||
26
config/migrations/0007_platform_income_log.py
Normal file
26
config/migrations/0007_platform_income_log.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0006_display_club_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PlatformIncomeLog',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('biz_ref', models.CharField(db_index=True, max_length=64, unique=True, verbose_name='业务单号')),
|
||||
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='入账金额')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '平台入账幂等日志',
|
||||
'verbose_name_plural': '平台入账幂等日志',
|
||||
'db_table': 'platform_income_log',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-04-04 18:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='clubconfig',
|
||||
name='storage_bucket_name',
|
||||
field=models.CharField(blank=True, default='', max_length=100, verbose_name='存储桶名称'),
|
||||
),
|
||||
]
|
||||
34
config/migrations/0008_gonggao_page_key.py
Normal file
34
config/migrations/0008_gonggao_page_key.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def backfill_gonggao_page_key(apps, schema_editor):
|
||||
Gonggao = apps.get_model('config', 'Gonggao')
|
||||
for row in Gonggao.objects.filter(page_key='').only('id'):
|
||||
row.page_key = 'order_pool'
|
||||
row.save(update_fields=['page_key'])
|
||||
for row in Gonggao.objects.filter(page_key__isnull=True).only('id'):
|
||||
row.page_key = 'order_pool'
|
||||
row.save(update_fields=['page_key'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0007_platform_income_log'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='gonggao',
|
||||
name='page_key',
|
||||
field=models.CharField(
|
||||
db_index=True, default='order_pool', max_length=32,
|
||||
verbose_name='页面标识(order_pool/accept_order/merchant_home)',
|
||||
),
|
||||
),
|
||||
migrations.RunPython(backfill_gonggao_page_key, migrations.RunPython.noop),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='gonggao',
|
||||
unique_together={('club_id', 'page_key')},
|
||||
),
|
||||
]
|
||||
@@ -1,53 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-04-05 13:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0008_clubconfig_storage_bucket_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DailyPayoutStat',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
|
||||
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
|
||||
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
|
||||
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
|
||||
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='出款总额')),
|
||||
('total_count', models.PositiveIntegerField(default=0, 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': 'daily_payout_stat',
|
||||
'indexes': [models.Index(fields=['date'], name='daily_payou_date_b534ec_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_payou_year_6376ca_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DailyIncomeStat',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
|
||||
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
|
||||
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
|
||||
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
|
||||
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='总收入金额')),
|
||||
('total_count', models.PositiveIntegerField(default=0, 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': 'daily_income_stat',
|
||||
'indexes': [models.Index(fields=['date'], name='daily_incom_date_75e6df_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_incom_year_d85d10_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
27
config/migrations/0009_gonggao_copy_pages.py
Normal file
27
config/migrations/0009_gonggao_copy_pages.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def copy_gonggao_to_other_pages(apps, schema_editor):
|
||||
"""将 order_pool 公告复制到 accept_order / merchant_home(仅当目标页尚无记录)。"""
|
||||
Gonggao = apps.get_model('config', 'Gonggao')
|
||||
for pk in ('accept_order', 'merchant_home'):
|
||||
for row in Gonggao.objects.filter(page_key='order_pool'):
|
||||
if Gonggao.objects.filter(club_id=row.club_id, page_key=pk).exists():
|
||||
continue
|
||||
Gonggao.objects.create(
|
||||
club_id=row.club_id,
|
||||
page_key=pk,
|
||||
NoticeType=row.NoticeType,
|
||||
Content=row.Content or '',
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0008_gonggao_page_key'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(copy_gonggao_to_other_pages, migrations.RunPython.noop),
|
||||
]
|
||||
67
config/migrations/0010_club_miniapp_icon_pindao.py
Normal file
67
config/migrations/0010_club_miniapp_icon_pindao.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0009_gonggao_copy_pages'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ClubMiniappIcon',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
|
||||
('icon_key', models.CharField(db_index=True, max_length=64, verbose_name='图标键')),
|
||||
('label', models.CharField(blank=True, default='', max_length=100, verbose_name='展示名称')),
|
||||
('image_path', models.CharField(blank=True, default='', max_length=500, verbose_name='图片相对路径')),
|
||||
('description', models.CharField(blank=True, default='', max_length=300, verbose_name='说明')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '小程序图标',
|
||||
'verbose_name_plural': '小程序图标',
|
||||
'db_table': 'club_miniapp_icon',
|
||||
'ordering': ['icon_key'],
|
||||
'unique_together': {('club_id', 'icon_key')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ClubPindao',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(default='xq', max_length=16, unique=True, verbose_name='俱乐部ID')),
|
||||
('channel_no', models.CharField(blank=True, default='', max_length=100, verbose_name='频道号')),
|
||||
('title', models.CharField(blank=True, default='频道', max_length=100, verbose_name='标题')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '俱乐部频道',
|
||||
'verbose_name_plural': '俱乐部频道',
|
||||
'db_table': 'club_pindao',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ClubPindaoImage',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
|
||||
('image_path', models.CharField(max_length=500, verbose_name='图片相对路径')),
|
||||
('sort_order', models.PositiveIntegerField(default=0, verbose_name='排序')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('pindao', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='images', to='config.clubpindao')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '频道图片',
|
||||
'verbose_name_plural': '频道图片',
|
||||
'db_table': 'club_pindao_image',
|
||||
'ordering': ['sort_order', 'id'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,84 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-04-22 11:24
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0009_dailypayoutstat_dailyincomestat'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PopupConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('popup_id', models.CharField(max_length=50, verbose_name='弹窗唯一ID')),
|
||||
('title', models.CharField(blank=True, max_length=200, verbose_name='标题(后台用)')),
|
||||
('description', models.TextField(blank=True, verbose_name='描述(后台用)')),
|
||||
('strategy_type', models.CharField(choices=[('once', '仅一次'), ('daily', '每日'), ('weekly', '每周'), ('monthly', '每月'), ('yearly', '每年'), ('always', '始终'), ('count', '周期内次数限制')], default='once', max_length=20)),
|
||||
('max_count', models.PositiveIntegerField(default=1, verbose_name='周期内最大展示次数')),
|
||||
('reset_interval', models.CharField(blank=True, choices=[('day', '天'), ('week', '周'), ('month', '月'), ('year', '年')], max_length=20, verbose_name='重置周期(count类型时使用)')),
|
||||
('duration', models.PositiveIntegerField(default=0, verbose_name='强制阅读秒数(0=无强制)')),
|
||||
('force_even_muted', models.BooleanField(default=False, verbose_name='强制弹出(忽略用户今日屏蔽)')),
|
||||
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('start_time', models.DateTimeField(blank=True, null=True, verbose_name='生效开始时间')),
|
||||
('end_time', models.DateTimeField(blank=True, null=True, verbose_name='生效结束时间')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '弹窗配置',
|
||||
'verbose_name_plural': '弹窗配置',
|
||||
'db_table': 'popup_config',
|
||||
'ordering': ['sort_order', 'id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PopupPage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('page_key', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='页面标识')),
|
||||
('name', models.CharField(max_length=100, verbose_name='页面名称')),
|
||||
('description', models.TextField(blank=True, verbose_name='描述')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('ignore_user_mute', models.BooleanField(default=False, verbose_name='忽略用户今日屏蔽')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '弹窗页面',
|
||||
'verbose_name_plural': '弹窗页面',
|
||||
'db_table': 'popup_page',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PopupImage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('image_url', models.CharField(max_length=500, verbose_name='图片相对路径')),
|
||||
('text', models.TextField(blank=True, verbose_name='图片上方文字')),
|
||||
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('popup_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='config.popupconfig')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '弹窗图片',
|
||||
'verbose_name_plural': '弹窗图片',
|
||||
'db_table': 'popup_image',
|
||||
'ordering': ['sort_order', 'id'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='popupconfig',
|
||||
name='page',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='popups', to='config.popuppage'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='popupconfig',
|
||||
unique_together={('page', 'popup_id')},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +1,16 @@
|
||||
# Generated by Django 4.2.27 on 2026-05-18 02:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0012_usershenheguan'),
|
||||
('config', '0010_club_miniapp_icon_pindao'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='usershenheguan',
|
||||
name='is_renzheng',
|
||||
field=models.BooleanField(default=False, verbose_name='是否认证'),
|
||||
model_name='clubminiappicon',
|
||||
name='is_uploaded',
|
||||
field=models.BooleanField(default=False, verbose_name='是否已在后台上传'),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-04-25 12:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WithdrawConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mode', models.IntegerField(choices=[(1, '自动提现'), (2, '手动提现')], default=2, verbose_name='提现模式')),
|
||||
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '提现模式配置',
|
||||
'verbose_name_plural': '提现模式配置',
|
||||
'db_table': 'withdraw_config',
|
||||
},
|
||||
),
|
||||
]
|
||||
52
config/migrations/0012_miniapp_script.py
Normal file
52
config/migrations/0012_miniapp_script.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0011_clubminiappicon_is_uploaded'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MiniappScriptScene',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
|
||||
('scene_key', models.CharField(db_index=True, max_length=64, verbose_name='场景键')),
|
||||
('title', models.CharField(blank=True, default='', max_length=128, verbose_name='标题')),
|
||||
('content', models.TextField(blank=True, default='', verbose_name='正文')),
|
||||
('confirm_text', models.CharField(blank=True, default='', max_length=16, verbose_name='确认按钮')),
|
||||
('cancel_text', models.CharField(blank=True, default='', max_length=16, verbose_name='取消按钮')),
|
||||
('enabled', models.BooleanField(default=True, verbose_name='启用')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '小程序场景话术',
|
||||
'verbose_name_plural': '小程序场景话术',
|
||||
'db_table': 'miniapp_script_scene',
|
||||
'unique_together': {('club_id', 'scene_key')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MiniappScriptAutoReply',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
|
||||
('keyword', models.CharField(max_length=200, verbose_name='关键词')),
|
||||
('reply_type', models.CharField(choices=[('text', '文本'), ('image', '图片')], default='text', max_length=16, verbose_name='回复类型')),
|
||||
('reply_content', models.TextField(verbose_name='回复内容(文本或图片URL)')),
|
||||
('sort_order', models.IntegerField(default=0, verbose_name='排序(大优先)')),
|
||||
('enabled', models.BooleanField(default=True, verbose_name='启用')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '客服自动回复规则',
|
||||
'verbose_name_plural': '客服自动回复规则',
|
||||
'db_table': 'miniapp_script_auto_reply',
|
||||
'ordering': ['-sort_order', 'id'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-05-17 18:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0011_withdrawconfig'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='shangjiamoban',
|
||||
name='chenghao_id',
|
||||
field=models.IntegerField(blank=True, null=True, verbose_name='关联称号ID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shangjiamoban',
|
||||
name='yongjin_jiage',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='佣金价格'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shangjiamoban',
|
||||
name='yongjin_qiangdan',
|
||||
field=models.BooleanField(default=False, verbose_name='是否开启佣金抢单'),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-05-26 03:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0012_shangjiamoban_chenghao_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='shangjialianjie',
|
||||
name='previous_dingdan_id',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='上一个订单ID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shangjialianjie',
|
||||
name='previous_lianjie_id',
|
||||
field=models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='上一个链接ID'),
|
||||
),
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-06-15 01:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0013_shangjialianjie_previous_dingdan_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='Club',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ClubConfig',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='DailyDispatchStat',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OrderTypeMapping',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tixianquotadefault',
|
||||
name='leixing',
|
||||
field=models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], unique=True, verbose_name='用户类型'),
|
||||
),
|
||||
]
|
||||
331
config/models.py
331
config/models.py
@@ -3,68 +3,90 @@ from gvsdsdk.model_base import QModel
|
||||
|
||||
|
||||
class Lunbo(QModel):
|
||||
tupian_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='轮播图片URL')
|
||||
leixing = models.IntegerField(null=True, blank=True, verbose_name='图片类型:1:轮播2:个人中心背景')
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
page_key = models.CharField(
|
||||
max_length=32, default='order_pool', db_index=True,
|
||||
verbose_name='页面标识(order_pool/accept_order/merchant_home/dashou_center)',
|
||||
)
|
||||
ImageURL = models.CharField(max_length=500, null=True, blank=True, verbose_name='轮播图片URL')
|
||||
ImageType = models.IntegerField(null=True, blank=True, verbose_name='图片类型:1:轮播2:个人中心背景')
|
||||
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
|
||||
class Meta:
|
||||
db_table = 'lunbo'
|
||||
verbose_name = '轮播'
|
||||
verbose_name_plural = '轮播'
|
||||
# 不要索引!数据量太小
|
||||
indexes = [
|
||||
models.Index(fields=['club_id', 'page_key', 'ImageType']),
|
||||
]
|
||||
|
||||
|
||||
class Gonggao(QModel):
|
||||
leixing = models.IntegerField(null=True, blank=True, verbose_name='公告类型')
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间')
|
||||
jieshao = models.TextField(null=True, blank=True, verbose_name='公告介绍')
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
page_key = models.CharField(
|
||||
max_length=32, default='order_pool', db_index=True,
|
||||
verbose_name='页面标识(order_pool/accept_order/merchant_home)',
|
||||
)
|
||||
NoticeType = models.IntegerField(null=True, blank=True, verbose_name='公告类型')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
|
||||
Content = models.TextField(null=True, blank=True, verbose_name='公告介绍')
|
||||
|
||||
class Meta:
|
||||
db_table = 'gonggao'
|
||||
verbose_name = '公告'
|
||||
verbose_name_plural = '公告'
|
||||
# 不要索引!数据量太小
|
||||
unique_together = [['club_id', 'page_key']]
|
||||
|
||||
class Qunpeizhi(QModel):
|
||||
peizhiid = models.IntegerField(null=True, blank=True, verbose_name='群类型:1.打手 2.管事')
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间')
|
||||
neirong = models.CharField(max_length=200, null=True, blank=True, verbose_name='群介绍')
|
||||
qunid = models.CharField(max_length=100, null=True, blank=True, verbose_name='群id')
|
||||
jieshao = models.CharField(max_length=200, null=True, blank=True, verbose_name='介绍')
|
||||
GroupType = models.IntegerField(null=True, blank=True, verbose_name='群类型:1.打手 2.管事')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
|
||||
GroupContent = models.CharField(max_length=200, null=True, blank=True, verbose_name='群介绍')
|
||||
GroupID = models.CharField(max_length=100, null=True, blank=True, verbose_name='群id')
|
||||
Description = models.CharField(max_length=200, null=True, blank=True, verbose_name='介绍')
|
||||
class Meta:
|
||||
db_table = 'qunpeizhi'
|
||||
|
||||
class Tupianpeizhi(QModel):
|
||||
tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='配置图片URL')
|
||||
leixing = models.PositiveIntegerField(null=True, blank=True, verbose_name='提现类型1:打手规则图片2:默认头像图片')
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间')
|
||||
fangwen_cishu = models.PositiveIntegerField(default=0, verbose_name='访问次数') # 新增字段
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
ImageURL = models.CharField(max_length=500, null=True, blank=True, verbose_name='配置图片URL')
|
||||
ImageType = models.PositiveIntegerField(null=True, blank=True, verbose_name='提现类型1:打手规则图片2:默认头像图片')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
|
||||
AccessCount = models.PositiveIntegerField(default=0, verbose_name='访问次数')
|
||||
class Meta:
|
||||
db_table = 'tupianpeizhi'
|
||||
verbose_name = '配置图片'
|
||||
verbose_name_plural = '配置图片'
|
||||
# ImageType 1/2 每俱乐部唯一;3(关注快手) 允许多张
|
||||
|
||||
|
||||
class Szjilu(QModel):
|
||||
"""
|
||||
【已废弃 · 禁止使用】
|
||||
|
||||
本表与财务看板「每日收入 / 每日支出」无关。
|
||||
财务四个板子只读 daily_income_stat、daily_payout_stat,不得读写本表。
|
||||
repair_wechat_income 曾误写本表导致数据膨胀,后续入账也不再更新本表。
|
||||
"""
|
||||
id = models.AutoField(primary_key=True, verbose_name='记录ID')
|
||||
zongsy = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总收益')
|
||||
zongls = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总流水')
|
||||
zongzc = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总支出')
|
||||
jrzc = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日支出')
|
||||
jrls = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日流水')
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间')
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
TotalIncome = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总收益')
|
||||
TotalFlow = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总流水')
|
||||
TotalExpense = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总支出')
|
||||
DailyExpense = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日支出')
|
||||
DailyFlow = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日流水')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
|
||||
class Meta:
|
||||
db_table = 'szjilu'
|
||||
verbose_name = '收支记录'
|
||||
verbose_name_plural = '收支记录'
|
||||
|
||||
|
||||
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['club_id'], name='uniq_szjilu_club_id'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -76,13 +98,12 @@ class ShangjiaMoban(QModel):
|
||||
id = models.AutoField(primary_key=True, verbose_name='模板ID')
|
||||
|
||||
# 关联字段
|
||||
yonghu_id = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
|
||||
leixing_id = models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')
|
||||
UserID = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
|
||||
ProductTypeID = models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')
|
||||
|
||||
# 模板内容
|
||||
|
||||
moban_jieshao = models.CharField(max_length=500, verbose_name='模板介绍')
|
||||
jiage = models.DecimalField(
|
||||
TemplateDesc = models.CharField(max_length=500, verbose_name='模板介绍')
|
||||
Price = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
verbose_name='模板价格',
|
||||
@@ -90,16 +111,16 @@ class ShangjiaMoban(QModel):
|
||||
)
|
||||
|
||||
# 统计字段
|
||||
fabu_shuliang = models.PositiveIntegerField(default=0, verbose_name='发布数量')
|
||||
PublishedCount = models.PositiveIntegerField(default=0, verbose_name='发布数量')
|
||||
|
||||
yongjin_qiangdan = models.BooleanField(default=False, verbose_name='是否开启佣金抢单')
|
||||
yongjin_jiage = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='佣金价格')
|
||||
chenghao_id = models.IntegerField(null=True, blank=True, verbose_name='关联称号ID')
|
||||
CommissionGrabEnabled = models.BooleanField(default=False, verbose_name='是否开启佣金抢单')
|
||||
CommissionPrice = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='佣金价格')
|
||||
TitleID = models.IntegerField(null=True, blank=True, verbose_name='关联称号ID')
|
||||
|
||||
|
||||
# 时间字段
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'shangjia_moban'
|
||||
@@ -107,15 +128,15 @@ class ShangjiaMoban(QModel):
|
||||
verbose_name_plural = verbose_name
|
||||
indexes = [
|
||||
# 高频查询组合索引
|
||||
models.Index(fields=['yonghu_id', 'leixing_id']),
|
||||
models.Index(fields=['yonghu_id', 'leixing_id','id']),
|
||||
models.Index(fields=['UserID', 'ProductTypeID']),
|
||||
models.Index(fields=['UserID', 'ProductTypeID','id']),
|
||||
|
||||
]
|
||||
# 同一个商家同一类型下模板介绍不能重复(允许修改)
|
||||
unique_together = ['yonghu_id', 'leixing_id', 'moban_jieshao']
|
||||
unique_together = ['UserID', 'ProductTypeID', 'TemplateDesc']
|
||||
|
||||
def __str__(self):
|
||||
return f"模板{self.id}-{self.yonghu_id}"
|
||||
return f"模板{self.id}-{self.UserID}"
|
||||
|
||||
|
||||
|
||||
@@ -127,50 +148,41 @@ class ShangjiaLianjie(QModel):
|
||||
id = models.AutoField(primary_key=True, verbose_name='链接ID')
|
||||
|
||||
# 关联字段
|
||||
yonghu_id = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
|
||||
leixing_id = models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')
|
||||
moban_id = models.PositiveIntegerField(db_index=True, verbose_name='模板ID')
|
||||
UserID = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
|
||||
ProductTypeID = models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')
|
||||
TemplateID = models.PositiveIntegerField(db_index=True, verbose_name='模板ID')
|
||||
|
||||
# 链接核心数据
|
||||
lianjie = models.CharField(max_length=500, unique=True, verbose_name='链接地址')
|
||||
#lianjie_token = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='链接Token')
|
||||
LinkURL = models.CharField(max_length=500, unique=True, verbose_name='链接地址')
|
||||
|
||||
# 订单关联(生成订单后更新)
|
||||
dingdan_id = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='订单ID')
|
||||
OrderID = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='订单ID')
|
||||
|
||||
# 使用状态
|
||||
is_used = models.BooleanField(default=False, verbose_name='是否已使用', db_index=True)
|
||||
#shiyong_time = models.DateTimeField(null=True, blank=True, verbose_name='使用时间')
|
||||
|
||||
# 使用者信息
|
||||
shiyongzhe_ip = models.CharField(max_length=50, null=True, blank=True, verbose_name='使用者IP')
|
||||
#shiyongzhe_info = models.TextField(null=True, blank=True, verbose_name='使用者信息')
|
||||
UserIP = models.CharField(max_length=50, null=True, blank=True, verbose_name='使用者IP')
|
||||
|
||||
# 链接控制
|
||||
#shiyong_cishu = models.PositiveIntegerField(default=1, verbose_name='可使用次数')
|
||||
#yixiang_cishu = models.PositiveIntegerField(default=0, verbose_name='已使用次数')
|
||||
youxiao_shijian = models.DateTimeField(null=True, blank=True, verbose_name='有效截止时间')
|
||||
ExpireTime = models.DateTimeField(null=True, blank=True, verbose_name='有效截止时间')
|
||||
|
||||
# 安全字段
|
||||
#anquan_ma = models.CharField(max_length=100, null=True, blank=True, verbose_name='安全码')
|
||||
# 新增 Token 字段
|
||||
lianjie_token = models.CharField(
|
||||
LinkToken = models.CharField(
|
||||
max_length=32,
|
||||
unique=True,
|
||||
db_index=True,
|
||||
verbose_name='链接Token',
|
||||
null=True,
|
||||
blank=True # 先设置为可选,后面再设为必填
|
||||
blank=True
|
||||
)
|
||||
|
||||
# 时间字段
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
# 在 ShangjiaLianjie 类中添加
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
# 上一个链接的ID(可选,记录订单被更换前的链接)
|
||||
previous_lianjie_id = models.PositiveIntegerField(
|
||||
PreviousLinkID = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
@@ -178,7 +190,7 @@ class ShangjiaLianjie(QModel):
|
||||
)
|
||||
|
||||
# 上一个订单的ID(可选,记录被更换的旧订单ID)
|
||||
previous_dingdan_id = models.CharField(
|
||||
PreviousOrderID = models.CharField(
|
||||
max_length=32,
|
||||
null=True,
|
||||
blank=True,
|
||||
@@ -192,19 +204,13 @@ class ShangjiaLianjie(QModel):
|
||||
verbose_name_plural = verbose_name
|
||||
indexes = [
|
||||
# 高频查询组合索引
|
||||
models.Index(fields=['yonghu_id', 'moban_id','leixing_id']),
|
||||
models.Index(fields=['lianjie_token']),
|
||||
#models.Index(fields=['lianjie_token']),
|
||||
models.Index(fields=['dingdan_id']),
|
||||
#models.Index(fields=['moban_id']),
|
||||
#models.Index(fields=['create_time']),
|
||||
#models.Index(fields=['youxiao_shijian', 'is_used']),
|
||||
models.Index(fields=['UserID', 'TemplateID','ProductTypeID']),
|
||||
models.Index(fields=['LinkToken']),
|
||||
models.Index(fields=['OrderID']),
|
||||
]
|
||||
#ordering = ['-create_time']
|
||||
|
||||
def __str__(self):
|
||||
return f"链接{self.id}-{self.yonghu_id}"
|
||||
|
||||
return f"链接{self.id}-{self.UserID}"
|
||||
|
||||
|
||||
|
||||
@@ -214,7 +220,7 @@ class TixianQuotaDefault(QModel):
|
||||
默认每日提现限额表
|
||||
"""
|
||||
id = models.AutoField(primary_key=True)
|
||||
leixing = models.IntegerField(
|
||||
UserType = models.IntegerField(
|
||||
choices=[(1, '打手'), (2, '管事'), (3, '组长'),
|
||||
(4, '考核官'), (5, '打手押金'), (6, '商家余额')],
|
||||
unique=True,
|
||||
@@ -226,8 +232,8 @@ class TixianQuotaDefault(QModel):
|
||||
default=20.00,
|
||||
verbose_name='默认每日提现限额(元)'
|
||||
)
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'tixian_quota_default'
|
||||
@@ -235,7 +241,7 @@ class TixianQuotaDefault(QModel):
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_leixing_display()} 默认限额: {self.default_quota}"
|
||||
return f"{self.get_UserType_display()} 默认限额: {self.default_quota}"
|
||||
|
||||
|
||||
|
||||
@@ -289,17 +295,18 @@ class AccountPermission(QModel):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# apps/dingdan/models.py (新增模型)
|
||||
|
||||
class DailyIncomeStat(QModel):
|
||||
"""
|
||||
每日收入统计表
|
||||
记录平台每天通过微信支付获得的总收入和总笔数
|
||||
每日收入统计表 —— 财务看板「今日收入 / 累计收入 / 每日明细」唯一数据源。
|
||||
微信支付成功时由 update_daily_income 写入;勿读 szjilu 表。
|
||||
"""
|
||||
# 日期相关字段(便于快速筛选和分组)
|
||||
date = models.DateField(verbose_name='统计日期', db_index=True, unique=True)
|
||||
date = models.DateField(verbose_name='统计日期', db_index=True)
|
||||
club_id = models.CharField(
|
||||
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
|
||||
)
|
||||
year = models.PositiveSmallIntegerField(verbose_name='年份')
|
||||
month = models.PositiveSmallIntegerField(verbose_name='月份')
|
||||
day = models.PositiveSmallIntegerField(verbose_name='日期')
|
||||
@@ -309,8 +316,8 @@ class DailyIncomeStat(QModel):
|
||||
total_count = models.PositiveIntegerField(default=0, verbose_name='总收入笔数')
|
||||
|
||||
# 时间戳
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'daily_income_stat'
|
||||
@@ -319,6 +326,10 @@ class DailyIncomeStat(QModel):
|
||||
indexes = [
|
||||
models.Index(fields=['date']),
|
||||
models.Index(fields=['year', 'month', 'day']),
|
||||
models.Index(fields=['club_id', 'date']),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['date', 'club_id'], name='uniq_daily_income_club_date'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
@@ -329,10 +340,12 @@ class DailyIncomeStat(QModel):
|
||||
|
||||
class DailyPayoutStat(QModel):
|
||||
"""
|
||||
每日出款统计表
|
||||
记录平台每天通过提现/结算等方式的出款总额和总笔数
|
||||
每日出款统计表 —— 财务看板「今日支出 / 累计支出」唯一数据源。
|
||||
"""
|
||||
date = models.DateField(verbose_name='统计日期', db_index=True, unique=True)
|
||||
date = models.DateField(verbose_name='统计日期', db_index=True)
|
||||
club_id = models.CharField(
|
||||
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
|
||||
)
|
||||
year = models.PositiveSmallIntegerField(verbose_name='年份')
|
||||
month = models.PositiveSmallIntegerField(verbose_name='月份')
|
||||
day = models.PositiveSmallIntegerField(verbose_name='日期')
|
||||
@@ -340,8 +353,8 @@ class DailyPayoutStat(QModel):
|
||||
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='出款总额')
|
||||
total_count = models.PositiveIntegerField(default=0, verbose_name='出款总笔数')
|
||||
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'daily_payout_stat'
|
||||
@@ -350,18 +363,32 @@ class DailyPayoutStat(QModel):
|
||||
indexes = [
|
||||
models.Index(fields=['date']),
|
||||
models.Index(fields=['year', 'month', 'day']),
|
||||
models.Index(fields=['club_id', 'date']),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['date', 'club_id'], name='uniq_daily_payout_club_date'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.date} - 出款{self.total_amount}元 - {self.total_count}笔"
|
||||
|
||||
|
||||
class PlatformIncomeLog(QModel):
|
||||
"""微信入账幂等日志:同一 biz_ref 只记一次 daily_income_stat。"""
|
||||
biz_ref = models.CharField(max_length=64, unique=True, db_index=True, verbose_name='业务单号')
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
amount = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='入账金额')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
|
||||
|
||||
class Meta:
|
||||
db_table = 'platform_income_log'
|
||||
verbose_name = '平台入账幂等日志'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
|
||||
class PopupPage(QModel):
|
||||
page_key = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='页面标识')
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
page_key = models.CharField(max_length=50, db_index=True, verbose_name='页面标识')
|
||||
name = models.CharField(max_length=100, verbose_name='页面名称')
|
||||
description = models.TextField(blank=True, verbose_name='描述')
|
||||
is_active = models.BooleanField(default=True, verbose_name='是否启用')
|
||||
@@ -373,11 +400,11 @@ class PopupPage(QModel):
|
||||
db_table = 'popup_page'
|
||||
verbose_name = '弹窗页面'
|
||||
verbose_name_plural = '弹窗页面'
|
||||
unique_together = [['club_id', 'page_key']]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name} ({self.page_key})'
|
||||
|
||||
|
||||
class PopupConfig(QModel):
|
||||
STRATEGY_CHOICES = (
|
||||
('once', '仅一次'),
|
||||
@@ -432,3 +459,111 @@ class PopupImage(QModel):
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.popup_config.popup_id} - 图片{self.sort_order}'
|
||||
|
||||
|
||||
class ClubMiniappIcon(QModel):
|
||||
"""小程序功能图标(按俱乐部,固定 icon_key + 存储桶相对路径)。"""
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
icon_key = models.CharField(max_length=64, db_index=True, verbose_name='图标键')
|
||||
label = models.CharField(max_length=100, blank=True, default='', verbose_name='展示名称')
|
||||
image_path = models.CharField(max_length=500, blank=True, default='', verbose_name='图片相对路径')
|
||||
description = models.CharField(max_length=300, blank=True, default='', verbose_name='说明')
|
||||
is_active = models.BooleanField(default=True, verbose_name='是否启用')
|
||||
is_uploaded = models.BooleanField(default=False, verbose_name='是否已在后台上传')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'club_miniapp_icon'
|
||||
verbose_name = '小程序图标'
|
||||
verbose_name_plural = verbose_name
|
||||
unique_together = [['club_id', 'icon_key']]
|
||||
ordering = ['icon_key']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.club_id}:{self.icon_key}'
|
||||
|
||||
|
||||
class ClubPindao(QModel):
|
||||
"""俱乐部频道配置(每俱乐部一条)。"""
|
||||
club_id = models.CharField(max_length=16, default='xq', unique=True, verbose_name='俱乐部ID')
|
||||
channel_no = models.CharField(max_length=100, blank=True, default='', verbose_name='频道号')
|
||||
title = models.CharField(max_length=100, blank=True, default='频道', verbose_name='标题')
|
||||
is_active = models.BooleanField(default=True, verbose_name='是否启用')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'club_pindao'
|
||||
verbose_name = '俱乐部频道'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.club_id} 频道'
|
||||
|
||||
|
||||
class ClubPindaoImage(QModel):
|
||||
"""频道展示图片(多张)。"""
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
pindao = models.ForeignKey(
|
||||
ClubPindao, on_delete=models.CASCADE, related_name='images', null=True, blank=True,
|
||||
)
|
||||
image_path = models.CharField(max_length=500, verbose_name='图片相对路径')
|
||||
sort_order = models.PositiveIntegerField(default=0, verbose_name='排序')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'club_pindao_image'
|
||||
verbose_name = '频道图片'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['sort_order', 'id']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.club_id} 频道图{self.sort_order}'
|
||||
|
||||
|
||||
class MiniappScriptScene(QModel):
|
||||
"""小程序固定场景话术(欢迎语、发图确认等)"""
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
scene_key = models.CharField(max_length=64, db_index=True, verbose_name='场景键')
|
||||
title = models.CharField(max_length=128, blank=True, default='', verbose_name='标题')
|
||||
content = models.TextField(blank=True, default='', verbose_name='正文')
|
||||
confirm_text = models.CharField(max_length=16, blank=True, default='', verbose_name='确认按钮')
|
||||
cancel_text = models.CharField(max_length=16, blank=True, default='', verbose_name='取消按钮')
|
||||
enabled = models.BooleanField(default=True, verbose_name='启用')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'miniapp_script_scene'
|
||||
verbose_name = '小程序场景话术'
|
||||
verbose_name_plural = verbose_name
|
||||
unique_together = [['club_id', 'scene_key']]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.club_id}:{self.scene_key}'
|
||||
|
||||
|
||||
class MiniappScriptAutoReply(QModel):
|
||||
"""客服关键词自动回复(支持文本/图片)"""
|
||||
REPLY_TEXT = 'text'
|
||||
REPLY_IMAGE = 'image'
|
||||
REPLY_TYPE_CHOICES = [(REPLY_TEXT, '文本'), (REPLY_IMAGE, '图片')]
|
||||
|
||||
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||
keyword = models.CharField(max_length=200, verbose_name='关键词')
|
||||
reply_type = models.CharField(max_length=16, default=REPLY_TEXT, choices=REPLY_TYPE_CHOICES, verbose_name='回复类型')
|
||||
reply_content = models.TextField(verbose_name='回复内容(文本或图片URL)')
|
||||
sort_order = models.IntegerField(default=0, verbose_name='排序(大优先)')
|
||||
enabled = models.BooleanField(default=True, verbose_name='启用')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'miniapp_script_auto_reply'
|
||||
verbose_name = '客服自动回复规则'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-sort_order', 'id']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.club_id}:{self.keyword}'
|
||||
|
||||
@@ -1,75 +1,33 @@
|
||||
"""
|
||||
阿龙电竞 - 收支记录定时任务
|
||||
处理收支记录表的每日清零
|
||||
固定使用ID=1的记录
|
||||
收支记录定时任务:每日清零各俱乐部今日流水/今日支出。
|
||||
"""
|
||||
from celery import shared_task
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
|
||||
# 导入模型
|
||||
from config.models import Szjilu
|
||||
from jituan.services.szjilu_accounting import reset_all_daily_szjilu
|
||||
from utils.celery_utils import log_task_execution, rollback_on_failure
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@shared_task
|
||||
@rollback_on_failure("收支记录每日清零")
|
||||
def daily_sz_reset_task():
|
||||
"""
|
||||
收支记录每日清零 - 每天凌晨0点5分执行
|
||||
清零收支记录表的今日相关字段,固定使用ID=1的记录
|
||||
"""
|
||||
"""收支记录每日清零 - 每天凌晨0点5分执行。"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# 获取今天的日期
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
logger.info(f"开始执行收支记录清零任务,日期: {today}")
|
||||
|
||||
# 查询ID=1的收支记录,如果不存在则创建
|
||||
try:
|
||||
szjilu = Szjilu.objects.get(id=1)
|
||||
logger.info(f"找到ID=1的收支记录")
|
||||
except Szjilu.DoesNotExist:
|
||||
# 创建ID=1的初始记录
|
||||
szjilu = Szjilu.objects.create(
|
||||
id=1,
|
||||
zongsy=Decimal('0.00'),
|
||||
zongls=Decimal('0.00'),
|
||||
zongzc=Decimal('0.00'),
|
||||
jrzc=Decimal('0.00'),
|
||||
jrls=Decimal('0.00')
|
||||
)
|
||||
logger.info(f"创建ID=1的收支记录")
|
||||
|
||||
# 记录清零前的值(用于日志)
|
||||
old_jrzc = szjilu.jrzc
|
||||
old_jrls = szjilu.jrls
|
||||
|
||||
# 清零今日字段
|
||||
szjilu.jrzc = Decimal('0.00')
|
||||
szjilu.jrls = Decimal('0.00')
|
||||
szjilu.save(update_fields=['jrzc', 'jrls', 'update_time'])
|
||||
|
||||
logger.info(f"收支记录清零完成,今日支出原值: {old_jrzc},今日流水原值: {old_jrls}")
|
||||
|
||||
result_msg = f"收支记录清零完成 - 日期: {today}, 记录ID: {szjilu.id}"
|
||||
|
||||
updated = reset_all_daily_szjilu()
|
||||
result_msg = f"收支记录清零完成 - 日期: {today}, 俱乐部记录数: {updated}"
|
||||
log_task_execution("收支记录每日清零", True, result_msg)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"date": today,
|
||||
"record_id": szjilu.id,
|
||||
"jrzc_cleared": True,
|
||||
"jrls_cleared": True,
|
||||
"old_jrzc": str(old_jrzc),
|
||||
"old_jrls": str(old_jrls),
|
||||
"message": result_msg
|
||||
"clubs_updated": updated,
|
||||
"message": result_msg,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -81,5 +39,5 @@ def daily_sz_reset_task():
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"date": datetime.now().strftime('%Y-%m-%d'),
|
||||
"message": error_msg
|
||||
"message": error_msg,
|
||||
}
|
||||
@@ -3,9 +3,9 @@ from django.urls import path
|
||||
from .views import ShangpinGonggaoView, AdminUpdateConfigView, AdminUploadImageView, AdminConfigQueryView, \
|
||||
DashouPaihangView,GetAdminZhanghaoView, GetXiugaiJiluView,ShangjiaTianjiaMobanView, ShangjiaMobanListView, \
|
||||
ShangjiaGengxinMobanView, ShangjiaShanchuMobanView, ShangjiaGenerateLinkView,KehuGetDingdanLianjieView, \
|
||||
KehuTianxieDingdanView,GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\
|
||||
GetDynamicConfigView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\
|
||||
ShangjiaLianjieListView
|
||||
KehuTianxieDingdanView, GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\
|
||||
GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\
|
||||
ShangjiaLianjieListView, HuashuHqView, HuashuMatchView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -32,10 +32,13 @@ urlpatterns = [
|
||||
path('guanshiewm', GuanshiQRCodeView.as_view(), name='管事生成二维码'),
|
||||
path('zuzhanghb', ZuzhangHaibaoView.as_view(), name='组长二维码'),
|
||||
path('qdpzhq', GetDynamicConfigView.as_view(), name='小程序获取配置'),
|
||||
path('haibaopz', HaibaoPeizhiView.as_view(), name='推广海报配置'),
|
||||
|
||||
path('tanchuang', PopupConfigView.as_view(), name='微信小程序获取弹窗数据'),
|
||||
path('hqtxzsym', GetWithdrawModeView.as_view(), name='小程序获取提现方式'),
|
||||
|
||||
path('yhbdsjh', CheckPhoneAuthView.as_view(), name='判断是否需要手机号绑定'),
|
||||
path('hqsjlslj', ShangjiaLianjieListView.as_view(), name='商家获取生成链接'),
|
||||
path('huashuhq', HuashuHqView.as_view(), name='小程序场景话术获取'),
|
||||
path('huashu_match', HuashuMatchView.as_view(), name='客服关键词自动回复'),
|
||||
]
|
||||
|
||||
3318
config/views.py
3318
config/views.py
File diff suppressed because it is too large
Load Diff
74
config/views/__init__.py
Normal file
74
config/views/__init__.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
配置应用视图聚合模块(config/views/__init__.py)
|
||||
|
||||
本模块原为 config/views.py(3718 行单文件),已按功能组拆分到 config/views/ 子包。
|
||||
本文件作为聚合 re-export 入口,重新导出所有视图类与公共符号。
|
||||
外部引用(config/urls.py、backend/urls.py、merchant_ops/views_templates.py)保持不变。
|
||||
|
||||
功能组拆分:
|
||||
- miniapp_config 小程序动态配置(动态配置/海报/弹窗/提现方式/手机号认证)
|
||||
- admin_config 管理员配置(商品公告/配置查询/图片上传/群与利率更新)
|
||||
- admin_stats 管理员统计/记录(打手排行/账号查询/修改记录)
|
||||
- merchant_templates 商家订单模板
|
||||
- merchant_links 商家链接/客户订单(含 IPUtils、OrderLinkThrottle)
|
||||
- qrcode_poster 二维码/海报(关注/管事二维码/组长海报)
|
||||
- scripts 话术配置(含辅助函数)
|
||||
"""
|
||||
import logging
|
||||
|
||||
# 各功能组视图
|
||||
from .miniapp_config import (
|
||||
GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView,
|
||||
GetWithdrawModeView, CheckPhoneAuthView,
|
||||
)
|
||||
from .admin_config import (
|
||||
ShangpinGonggaoView, AdminConfigQueryView, AdminUploadImageView,
|
||||
AdminUpdateConfigView,
|
||||
)
|
||||
from .admin_stats import (
|
||||
DashouPaihangView, GetAdminZhanghaoView, GetXiugaiJiluView,
|
||||
)
|
||||
from .merchant_templates import (
|
||||
ShangjiaMobanListView, ShangjiaTianjiaMobanView,
|
||||
ShangjiaShanchuMobanView, ShangjiaGengxinMobanView,
|
||||
)
|
||||
from .merchant_links import (
|
||||
ShangjiaGenerateLinkView, ShangjiaLianjieListView,
|
||||
KehuGetDingdanLianjieView, KehuTianxieDingdanView,
|
||||
IPUtils, OrderLinkThrottle,
|
||||
)
|
||||
from .qrcode_poster import (
|
||||
GuanZhuAListView, GuanshiQRCodeView, ZuzhangHaibaoView,
|
||||
)
|
||||
from .scripts import (
|
||||
HuashuHqView, HuashuMatchView,
|
||||
MiniappScriptHoutaiListView, MiniappScriptHoutaiModifyView,
|
||||
)
|
||||
|
||||
# 模块级符号(保持向后兼容)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
# miniapp_config
|
||||
'GetDynamicConfigView', 'HaibaoPeizhiView', 'PopupConfigView',
|
||||
'GetWithdrawModeView', 'CheckPhoneAuthView',
|
||||
# admin_config
|
||||
'ShangpinGonggaoView', 'AdminConfigQueryView', 'AdminUploadImageView',
|
||||
'AdminUpdateConfigView',
|
||||
# admin_stats
|
||||
'DashouPaihangView', 'GetAdminZhanghaoView', 'GetXiugaiJiluView',
|
||||
# merchant_templates
|
||||
'ShangjiaMobanListView', 'ShangjiaTianjiaMobanView',
|
||||
'ShangjiaShanchuMobanView', 'ShangjiaGengxinMobanView',
|
||||
# merchant_links
|
||||
'ShangjiaGenerateLinkView', 'ShangjiaLianjieListView',
|
||||
'KehuGetDingdanLianjieView', 'KehuTianxieDingdanView',
|
||||
'IPUtils', 'OrderLinkThrottle',
|
||||
# qrcode_poster
|
||||
'GuanZhuAListView', 'GuanshiQRCodeView', 'ZuzhangHaibaoView',
|
||||
# scripts
|
||||
'HuashuHqView', 'HuashuMatchView',
|
||||
'MiniappScriptHoutaiListView', 'MiniappScriptHoutaiModifyView',
|
||||
# 公共符号
|
||||
'logger',
|
||||
]
|
||||
625
config/views/admin_config.py
Normal file
625
config/views/admin_config.py
Normal file
@@ -0,0 +1,625 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
import secrets
|
||||
import hashlib
|
||||
import threading
|
||||
import urllib.parse
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Q, F, Max, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.core.cache import cache
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils import timezone
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||
|
||||
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||
# from utils.ip_security import *
|
||||
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||
|
||||
from backend.utils import update_shangjia_daily
|
||||
from utils.chat_utils import subscribe_merchant_link_chat
|
||||
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||
|
||||
from ..models import (
|
||||
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||
MiniappScriptScene, MiniappScriptAutoReply,
|
||||
)
|
||||
|
||||
from users.models import (
|
||||
AdminProfile, UserDashou, UserBoss,
|
||||
UserShangjia, UserGuanshi, UserZuzhang
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
|
||||
from rank.models import Chenghao, DingdanBiaoqian
|
||||
|
||||
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||
|
||||
from ..serializers import PopupConfigSerializer
|
||||
import traceback
|
||||
import requests
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ShangpinGonggaoView(APIView):
|
||||
"""
|
||||
商品公告和轮播图接口
|
||||
获取类型为1的公告和轮播图
|
||||
"""
|
||||
throttle_classes = [AnonRateThrottle]
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = []
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
处理POST请求,返回公告和轮播图数据
|
||||
|
||||
请求: POST /api/peizhi/shangpingonggao/
|
||||
返回: {
|
||||
"shangpingonggao": "公告内容字符串",
|
||||
"shangpinlunbo": ["轮播图URL1", "轮播图URL2"]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
logger.info("收到商品公告和轮播图请求")
|
||||
from jituan.services.display_config import get_gonggao_content, get_lunbo_urls, normalize_page_key
|
||||
|
||||
page_key = normalize_page_key(request.data.get('page_key'), image_type=1)
|
||||
gonggao_content = get_gonggao_content(request, notice_type=1, page_key=page_key)
|
||||
lunbo_urls = get_lunbo_urls(request, page_key=page_key, image_type=1)
|
||||
response_data = {
|
||||
"shangpingonggao": gonggao_content,
|
||||
"shangpinlunbo": lunbo_urls,
|
||||
"page_key": page_key,
|
||||
}
|
||||
logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)} page_key={page_key}")
|
||||
# 4. 返回响应
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
# 记录异常
|
||||
logger.error(f"获取商品公告和轮播图时发生错误:{str(e)}")
|
||||
# 返回错误响应
|
||||
error_data = {
|
||||
"error": "服务器内部错误",
|
||||
"detail": "获取数据失败,请稍后重试"
|
||||
}
|
||||
return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
|
||||
class AdminConfigQueryView(APIView):
|
||||
"""
|
||||
管理员获取系统配置接口
|
||||
URL: /peizhi/adpzhq
|
||||
方法: POST
|
||||
权限: JWT Token认证 + 管理员权限
|
||||
参数: zhanghao (管理员账号)
|
||||
"""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 获取前端传递的账号
|
||||
zhanghao = request.data.get('zhanghao')
|
||||
if not zhanghao:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '账号不能为空',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 获取当前用户(User实例)
|
||||
user_main = request.user
|
||||
|
||||
# 验证账号:比较user_main的phone与前端传递的zhanghao
|
||||
if user_main.Phone != zhanghao:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '账号不匹配',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 验证用户类型是否为管理员
|
||||
if user_main.UserType != 'admin':
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '无权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 验证管理员扩展表是否存在
|
||||
try:
|
||||
admin_profile = user_main.AdminProfile
|
||||
except AdminProfile.DoesNotExist:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '无权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 查询图片配置
|
||||
rule_image = Tupianpeizhi.query.filter(ImageType=1).first()
|
||||
default_avatar = Tupianpeizhi.query.filter(ImageType=2).first()
|
||||
|
||||
# 查询群配置
|
||||
qun_configs = Qunpeizhi.query.all()
|
||||
qq_groups = []
|
||||
for config in qun_configs:
|
||||
qq_groups.append({
|
||||
'peizhiid': config.GroupType,
|
||||
'neirong': config.GroupContent if config.GroupContent else '',
|
||||
'qunid': config.GroupID if config.GroupID else '',
|
||||
'jieshao': config.Description if config.Description else ''
|
||||
})
|
||||
|
||||
# 查询利率配置
|
||||
platform_rate_obj = CommissionRate.query.filter(Platform='1').first()
|
||||
shop_rate_obj = CommissionRate.query.filter(Platform='3').first()
|
||||
# 🔴【新增】打手提现抽成利率(字符5)
|
||||
dashou_withdraw_obj = CommissionRate.query.filter(Platform='5').first()
|
||||
# 🔴【新增】管事提现抽成利率(字符6)
|
||||
guanshi_withdraw_obj = CommissionRate.query.filter(Platform='6').first()
|
||||
|
||||
# 构建返回数据
|
||||
data = {
|
||||
'images': {
|
||||
'rule_image': rule_image.ImageURL if rule_image and rule_image.ImageURL else '',
|
||||
'default_avatar': default_avatar.ImageURL if default_avatar and default_avatar.ImageURL else ''
|
||||
},
|
||||
'qq_groups': qq_groups,
|
||||
'rates': {
|
||||
'platform_rate': float(platform_rate_obj.Rate) if platform_rate_obj and platform_rate_obj.Rate else 0.0,
|
||||
'shop_rate': float(shop_rate_obj.Rate) if shop_rate_obj and shop_rate_obj.Rate else 0.0,
|
||||
# 🔴【新增】打手提现抽成利率
|
||||
'dashou_withdraw_rate': float(
|
||||
dashou_withdraw_obj.Rate) if dashou_withdraw_obj and dashou_withdraw_obj.Rate else 0.0,
|
||||
# 🔴【新增】管事提现抽成利率
|
||||
'guanshi_withdraw_rate': float(
|
||||
guanshi_withdraw_obj.Rate) if guanshi_withdraw_obj and guanshi_withdraw_obj.Rate else 0.0
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return Response({
|
||||
'code': 99,
|
||||
'msg': f'系统错误: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class AdminUploadImageView(APIView):
|
||||
"""
|
||||
管理员上传配置图片接口
|
||||
URL: /peizhi/adxgtp
|
||||
方法: POST
|
||||
权限: JWT Token认证 + 管理员权限
|
||||
参数: zhanghao, leixing (1:打手规则图片, 2:默认头像图片), image (文件)
|
||||
"""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
zhanghao = request.data.get('zhanghao')
|
||||
leixing = request.data.get('leixing')
|
||||
image_file = request.FILES.get('image')
|
||||
|
||||
if not zhanghao:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '账号不能为空',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not leixing:
|
||||
return Response({
|
||||
'code': 2,
|
||||
'msg': '图片类型不能为空',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not image_file:
|
||||
return Response({
|
||||
'code': 3,
|
||||
'msg': '请选择图片文件',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 验证用户权限
|
||||
user_main = request.user
|
||||
if user_main.Phone != zhanghao:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '账号不匹配',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
if user_main.UserType != 'admin':
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '无权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
try:
|
||||
admin_profile = user_main.AdminProfile
|
||||
except AdminProfile.DoesNotExist:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '无权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 验证图片类型
|
||||
if leixing not in ['1', '2']:
|
||||
return Response({
|
||||
'code': 4,
|
||||
'msg': '图片类型错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 验证图片文件
|
||||
is_valid, error_msg = validate_image(image_file)
|
||||
if not is_valid:
|
||||
return Response({
|
||||
'code': 5,
|
||||
'msg': f'图片验证失败: {error_msg}',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 根据类型确定固定相对URL和文件名
|
||||
if leixing == '1':
|
||||
# 打手规则图片
|
||||
fixed_relative_url = 'a_long/dashouguize.jpg'
|
||||
oss_file_name = 'dashouguize.jpg'
|
||||
else:
|
||||
# 默认头像图片
|
||||
fixed_relative_url = 'a_long/morentouxiang.jpg'
|
||||
oss_file_name = 'morentouxiang.jpg'
|
||||
|
||||
# 构建OSS文件路径
|
||||
oss_file_path = fixed_relative_url # a_long/dashouguize.jpg 或 a_long/morentouxiang.jpg
|
||||
|
||||
# 上传到OSS(覆盖原有文件)
|
||||
image_file.seek(0) # 重置文件指针
|
||||
full_url = upload_to_oss(image_file, oss_file_path)
|
||||
|
||||
if not full_url:
|
||||
return Response({
|
||||
'code': 6,
|
||||
'msg': '图片上传到存储桶失败',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# 更新数据库
|
||||
with transaction.atomic():
|
||||
# 获取或创建图片配置记录
|
||||
tupian_peizhi, created = Tupianpeizhi.query.get_or_create(ImageType=int(leixing))
|
||||
tupian_peizhi.ImageURL = fixed_relative_url
|
||||
tupian_peizhi.save()
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '图片上传成功',
|
||||
'data': {
|
||||
'image_url': fixed_relative_url
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return Response({
|
||||
'code': 99,
|
||||
'msg': f'系统错误: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class AdminUpdateConfigView(APIView):
|
||||
"""
|
||||
管理员更新系统配置接口(QQ群、利率)- 已修复
|
||||
URL: /peizhi/adupdate
|
||||
方法: POST
|
||||
权限: JWT Token认证 + 管理员权限
|
||||
参数:
|
||||
- zhanghao: 管理员账号
|
||||
- qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选)
|
||||
- rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选)
|
||||
"""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
zhanghao = request.data.get('zhanghao')
|
||||
if not zhanghao:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '账号不能为空',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 验证用户权限
|
||||
user_main = request.user
|
||||
if user_main.Phone != zhanghao:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '账号不匹配',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
if user_main.UserType != 'admin':
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '无权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
try:
|
||||
admin_profile = user_main.AdminProfile
|
||||
except AdminProfile.DoesNotExist:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '无权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
response_data = {}
|
||||
|
||||
# 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录
|
||||
qq_groups = request.data.get('qq_groups')
|
||||
if qq_groups and isinstance(qq_groups, list):
|
||||
# 收集所有 peizhiid,批量预取已有记录(避免循环内 N+1)
|
||||
_peizhiids = [g.get('peizhiid') for g in qq_groups if g.get('peizhiid')]
|
||||
_existing_map = {}
|
||||
if _peizhiids:
|
||||
for q in Qunpeizhi.query.filter(GroupType__in=_peizhiids):
|
||||
# 如果存在多个相同 peizhiid 的记录,取第一个(应该只有一个)
|
||||
if q.GroupType not in _existing_map:
|
||||
_existing_map[q.GroupType] = q
|
||||
|
||||
_to_create = []
|
||||
_to_update = []
|
||||
updated_groups = []
|
||||
for group_data in qq_groups:
|
||||
peizhiid = group_data.get('peizhiid')
|
||||
if not peizhiid:
|
||||
continue
|
||||
|
||||
neirong = group_data.get('neirong')
|
||||
qunid = group_data.get('qunid')
|
||||
jieshao = group_data.get('jieshao')
|
||||
|
||||
qun_config = _existing_map.get(peizhiid)
|
||||
if qun_config is not None:
|
||||
# 更新已有记录
|
||||
if neirong is not None:
|
||||
qun_config.GroupContent = neirong or ''
|
||||
if qunid is not None:
|
||||
qun_config.GroupID = qunid or ''
|
||||
if jieshao is not None:
|
||||
qun_config.Description = jieshao or ''
|
||||
_to_update.append(qun_config)
|
||||
else:
|
||||
# 不存在则创建新的
|
||||
qun_config = Qunpeizhi(
|
||||
GroupType=peizhiid,
|
||||
GroupContent=neirong or '',
|
||||
GroupID=qunid or '',
|
||||
Description=jieshao or '',
|
||||
)
|
||||
_to_create.append(qun_config)
|
||||
|
||||
updated_groups.append({
|
||||
'peizhiid': qun_config.GroupType,
|
||||
'neirong': qun_config.GroupContent,
|
||||
'qunid': qun_config.GroupID,
|
||||
'jieshao': qun_config.Description
|
||||
})
|
||||
|
||||
if _to_create:
|
||||
Qunpeizhi.objects.bulk_create(_to_create)
|
||||
if _to_update:
|
||||
Qunpeizhi.objects.bulk_update(
|
||||
_to_update, ['GroupContent', 'GroupID', 'Description']
|
||||
)
|
||||
|
||||
if updated_groups:
|
||||
response_data['qq_groups'] = updated_groups
|
||||
|
||||
# 🔴 修复:更新利率配置 - 确保更新现有记录而不是创建新记录
|
||||
rates = request.data.get('rates')
|
||||
if rates and isinstance(rates, dict):
|
||||
updated_rates = {}
|
||||
|
||||
# 平台发单利率(类型1,fadanpingtai='1')
|
||||
platform_rate = rates.get('platform_rate')
|
||||
if platform_rate is not None:
|
||||
try:
|
||||
platform_rate_float = float(platform_rate)
|
||||
if 0 <= platform_rate_float <= 1:
|
||||
# 🔴 修复:查询现有记录,如果不存在则创建
|
||||
platform_rate_obj = CommissionRate.query.filter(Platform='1').first()
|
||||
|
||||
if platform_rate_obj:
|
||||
# 更新现有记录
|
||||
platform_rate_obj.Rate = platform_rate_float
|
||||
platform_rate_obj.save()
|
||||
else:
|
||||
# 创建新记录
|
||||
platform_rate_obj = CommissionRate.query.create(
|
||||
Rate=platform_rate_float,
|
||||
Platform='1'
|
||||
)
|
||||
|
||||
updated_rates['platform_rate'] = platform_rate_float
|
||||
else:
|
||||
return Response({
|
||||
'code': 3,
|
||||
'msg': '平台发单利率必须在0-1之间',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except ValueError:
|
||||
return Response({
|
||||
'code': 4,
|
||||
'msg': '平台发单利率格式错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 商家发单利率(类型2,fadanpingtai='2')
|
||||
shop_rate = rates.get('shop_rate')
|
||||
if shop_rate is not None:
|
||||
try:
|
||||
shop_rate_float = float(shop_rate)
|
||||
if 0 <= shop_rate_float <= 1:
|
||||
# 🔴 修复:查询现有记录,如果不存在则创建
|
||||
shop_rate_obj = CommissionRate.query.filter(Platform='3').first()
|
||||
|
||||
if shop_rate_obj:
|
||||
# 更新现有记录
|
||||
shop_rate_obj.Rate = shop_rate_float
|
||||
shop_rate_obj.save()
|
||||
else:
|
||||
# 创建新记录
|
||||
shop_rate_obj = CommissionRate.query.create(
|
||||
Rate=shop_rate_float,
|
||||
Platform='3'
|
||||
)
|
||||
|
||||
updated_rates['shop_rate'] = shop_rate_float
|
||||
else:
|
||||
return Response({
|
||||
'code': 5,
|
||||
'msg': '商家发单利率必须在0-1之间',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except ValueError:
|
||||
return Response({
|
||||
'code': 6,
|
||||
'msg': '商家发单利率格式错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
|
||||
|
||||
# 🔴【新增】打手提现抽成利率(类型5,fadanpingtai='5')
|
||||
dashou_withdraw_rate = rates.get('dashou_withdraw_rate')
|
||||
if dashou_withdraw_rate is not None:
|
||||
try:
|
||||
dashou_withdraw_rate_float = float(dashou_withdraw_rate)
|
||||
if 0 <= dashou_withdraw_rate_float <= 1:
|
||||
# 查询现有记录,如果不存在则创建
|
||||
dashou_withdraw_obj = CommissionRate.query.filter(Platform='5').first()
|
||||
|
||||
if dashou_withdraw_obj:
|
||||
# 更新现有记录
|
||||
dashou_withdraw_obj.Rate = dashou_withdraw_rate_float
|
||||
dashou_withdraw_obj.save()
|
||||
else:
|
||||
# 创建新记录
|
||||
dashou_withdraw_obj = CommissionRate.query.create(
|
||||
Rate=dashou_withdraw_rate_float,
|
||||
Platform='5'
|
||||
)
|
||||
|
||||
updated_rates['dashou_withdraw_rate'] = dashou_withdraw_rate_float
|
||||
else:
|
||||
return Response({
|
||||
'code': 7, # 注意:这里需要调整错误码,不要重复
|
||||
'msg': '打手提现抽成利率必须在0-1之间',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except ValueError:
|
||||
return Response({
|
||||
'code': 8,
|
||||
'msg': '打手提现抽成利率格式错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 🔴【新增】管事提现抽成利率(类型6,fadanpingtai='6')
|
||||
guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate')
|
||||
if guanshi_withdraw_rate is not None:
|
||||
try:
|
||||
guanshi_withdraw_rate_float = float(guanshi_withdraw_rate)
|
||||
if 0 <= guanshi_withdraw_rate_float <= 1:
|
||||
# 查询现有记录,如果不存在则创建
|
||||
guanshi_withdraw_obj = CommissionRate.query.filter(Platform='6').first()
|
||||
|
||||
if guanshi_withdraw_obj:
|
||||
# 更新现有记录
|
||||
guanshi_withdraw_obj.Rate = guanshi_withdraw_rate_float
|
||||
guanshi_withdraw_obj.save()
|
||||
else:
|
||||
# 创建新记录
|
||||
guanshi_withdraw_obj = CommissionRate.query.create(
|
||||
Rate=guanshi_withdraw_rate_float,
|
||||
Platform='6'
|
||||
)
|
||||
|
||||
updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float
|
||||
else:
|
||||
return Response({
|
||||
'code': 9,
|
||||
'msg': '管事提现抽成利率必须在0-1之间',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except ValueError:
|
||||
return Response({
|
||||
'code': 10,
|
||||
'msg': '管事提现抽成利率格式错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if updated_rates:
|
||||
response_data['rates'] = updated_rates
|
||||
|
||||
# 如果没有任何更新
|
||||
if not response_data:
|
||||
return Response({
|
||||
'code': 7,
|
||||
'msg': '没有需要更新的数据',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '更新成功',
|
||||
'data': response_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return Response({
|
||||
'code': 99,
|
||||
'msg': f'系统错误: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
|
||||
730
config/views/admin_stats.py
Normal file
730
config/views/admin_stats.py
Normal file
@@ -0,0 +1,730 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
import secrets
|
||||
import hashlib
|
||||
import threading
|
||||
import urllib.parse
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Q, F, Max, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.core.cache import cache
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils import timezone
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||
|
||||
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||
# from utils.ip_security import *
|
||||
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||
|
||||
from backend.utils import update_shangjia_daily
|
||||
from utils.chat_utils import subscribe_merchant_link_chat
|
||||
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||
|
||||
from ..models import (
|
||||
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||
MiniappScriptScene, MiniappScriptAutoReply,
|
||||
)
|
||||
|
||||
from users.models import (
|
||||
AdminProfile, UserDashou, UserBoss,
|
||||
UserShangjia, UserGuanshi, UserZuzhang
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
|
||||
from rank.models import Chenghao, DingdanBiaoqian
|
||||
|
||||
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||
|
||||
from ..serializers import PopupConfigSerializer
|
||||
import traceback
|
||||
import requests
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DashouPaihangView(APIView):
|
||||
"""
|
||||
排行榜接口
|
||||
路径:/peizhi/dsph
|
||||
方法:POST
|
||||
认证:JWT Token认证
|
||||
|
||||
请求参数:
|
||||
- leixing: 1=今日收入,2=今月收入
|
||||
- yonghu_leixing: 1=打手,2=管事,3=商家
|
||||
|
||||
返回数据:
|
||||
- code: 0=成功,其他=失败
|
||||
- msg: 提示信息
|
||||
- data: {
|
||||
xianshi: True/False, # 是否展示真实数据
|
||||
paihang_list: [ # 排行榜列表
|
||||
{
|
||||
uid: '用户ID',
|
||||
touxiang: '头像相对URL',
|
||||
nicheng: '用户昵称',
|
||||
jine: 收入金额,
|
||||
mingci: 排名
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# =============== 可配置参数 ===============
|
||||
# 每次查询返回的最大记录数(可修改)
|
||||
ZUI_DAO_CHA_XUN_SHU_LIANG = 50
|
||||
|
||||
# 最少需要多少人才能展示(可修改)
|
||||
ZUI_SHAO_REN_SHU_YAO_QIU = 1
|
||||
|
||||
# 第一名最低金额要求(单位:元)(可修改)
|
||||
DI_YI_MING_ZUI_DI_JIN_E = Decimal('10.00')
|
||||
|
||||
# 管事第一名最低数量要求(可修改)
|
||||
DI_YI_MING_ZUI_DI_SHU_LIANG = 1
|
||||
# =============== 可配置参数结束 ===============
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 获取请求参数 - 修复参数类型问题
|
||||
leixing_str = request.data.get('leixing', '1')
|
||||
yonghu_leixing_str = request.data.get('yonghu_leixing', '1')
|
||||
|
||||
# 转换为整数
|
||||
try:
|
||||
leixing = int(leixing_str)
|
||||
except (ValueError, TypeError):
|
||||
leixing = 1
|
||||
|
||||
try:
|
||||
yonghu_leixing = int(yonghu_leixing_str)
|
||||
except (ValueError, TypeError):
|
||||
yonghu_leixing = 1
|
||||
|
||||
# 参数验证
|
||||
if leixing not in [1, 2]:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'msg': '参数错误:leixing必须为1(今日)或2(今月)',
|
||||
'data': {'xianshi': False}
|
||||
})
|
||||
|
||||
if yonghu_leixing not in [1, 2, 3]:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'msg': '参数错误:yonghu_leixing必须为1(打手)、2(管事)或3(商家)',
|
||||
'data': {'xianshi': False}
|
||||
})
|
||||
|
||||
# 根据用户类型调用不同的查询方法
|
||||
if yonghu_leixing == 1:
|
||||
# 打手排行榜
|
||||
paihang_list, xianshi_flag = self.chaxun_dashou_paihang(leixing)
|
||||
elif yonghu_leixing == 2:
|
||||
# 管事排行榜
|
||||
paihang_list, xianshi_flag = self.chaxun_guanshi_paihang(leixing)
|
||||
else:
|
||||
# 商家排行榜
|
||||
paihang_list, xianshi_flag = self.chaxun_shangjia_paihang(leixing)
|
||||
|
||||
# 返回结果
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '获取成功',
|
||||
'data': {
|
||||
'xianshi': xianshi_flag,
|
||||
'paihang_list': paihang_list
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"排行榜查询失败: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'msg': '服务器内部错误',
|
||||
'data': {'xianshi': False}
|
||||
})
|
||||
|
||||
def chaxun_dashou_paihang(self, leixing):
|
||||
"""
|
||||
查询打手排行榜
|
||||
:param leixing: 1=今日收入,2=今月收入
|
||||
:return: (排行榜列表, 是否展示标志)
|
||||
"""
|
||||
# 确定查询的字段
|
||||
if leixing == 1:
|
||||
# 今日收入
|
||||
shouyi_ziduan = 'jinrishouyi'
|
||||
else:
|
||||
# 今月收入
|
||||
shouyi_ziduan = 'jinyueshouyi'
|
||||
|
||||
# 查询条件:收入大于0,打手账号状态正常(1=正常)
|
||||
# 注意:打手表中的zhanghaozhuangtai字段
|
||||
chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1)
|
||||
|
||||
# 查询打手扩展表,并关联用户主表
|
||||
dashou_list = UserDashou.query.select_related('user').filter(
|
||||
chaxun_tiaojian
|
||||
).order_by(f'-{shouyi_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
|
||||
|
||||
# 检查是否满足展示条件
|
||||
if len(dashou_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
|
||||
# 人数不足,不展示
|
||||
return [], False
|
||||
|
||||
# 检查第一名金额是否达到最低要求
|
||||
if leixing == 1:
|
||||
di_yi_ming_jine = dashou_list[0].jinrishouyi
|
||||
else:
|
||||
di_yi_ming_jine = dashou_list[0].jinyueshouyi
|
||||
|
||||
if di_yi_ming_jine < self.DI_YI_MING_ZUI_DI_JIN_E:
|
||||
# 第一名金额不足,不展示
|
||||
return [], False
|
||||
|
||||
# 构建返回数据
|
||||
paihang_list = []
|
||||
for index, dashou in enumerate(dashou_list):
|
||||
# 获取用户主表信息
|
||||
user_main = dashou.user
|
||||
|
||||
# 打手的昵称直接使用打手扩展表的nicheng字段
|
||||
dashou_nicheng = dashou.nicheng or '未设置昵称'
|
||||
|
||||
# 获取头像(相对URL)
|
||||
touxiang_url = user_main.Avatar or ''
|
||||
|
||||
# 获取收入金额
|
||||
if leixing == 1:
|
||||
shouyi_jine = dashou.jinrishouyi
|
||||
else:
|
||||
shouyi_jine = dashou.jinyueshouyi
|
||||
|
||||
paihang_list.append({
|
||||
'uid': user_main.UserUID,
|
||||
'touxiang': touxiang_url,
|
||||
'nicheng': dashou_nicheng,
|
||||
'jine': float(shouyi_jine), # 转换为float方便前端处理
|
||||
'mingci': index + 1
|
||||
})
|
||||
|
||||
return paihang_list, True
|
||||
|
||||
def chaxun_guanshi_paihang(self, leixing):
|
||||
"""
|
||||
查询管事排行榜
|
||||
:param leixing: 1=今日,2=今月
|
||||
:return: (排行榜列表, 是否展示标志)
|
||||
"""
|
||||
# 确定查询的字段
|
||||
if leixing == 1:
|
||||
# 今日充值打手数量
|
||||
chaxun_ziduan = 'jinrichongzhi'
|
||||
else:
|
||||
# 今月充值打手数量
|
||||
chaxun_ziduan = 'jinyuechongzhi'
|
||||
|
||||
# 查询条件:数量大于0,管事状态正常(状态1为正常)
|
||||
# 注意:管手表中的zhuangtai字段
|
||||
chaxun_tiaojian = Q(**{f'{chaxun_ziduan}__gt': 0}) & Q(zhuangtai=1)
|
||||
|
||||
# 查询管事扩展表,并关联用户主表
|
||||
guanshi_list = UserGuanshi.query.select_related('user').filter(
|
||||
chaxun_tiaojian
|
||||
).order_by(f'-{chaxun_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
|
||||
|
||||
# 检查是否满足展示条件
|
||||
if len(guanshi_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
|
||||
# 人数不足,不展示
|
||||
return [], False
|
||||
|
||||
# 检查第一名数量是否达到最低要求
|
||||
if leixing == 1:
|
||||
di_yi_ming_shuliang = guanshi_list[0].jinrichongzhi
|
||||
else:
|
||||
di_yi_ming_shuliang = guanshi_list[0].jinyuechongzhi
|
||||
|
||||
if di_yi_ming_shuliang < self.DI_YI_MING_ZUI_DI_SHU_LIANG:
|
||||
# 第一名数量不足,不展示
|
||||
return [], False
|
||||
|
||||
# 批量预取老板扩展表昵称
|
||||
_boss_user_ids = [g.user_id for g in guanshi_list]
|
||||
_boss_nicheng_map = {
|
||||
b.user_id: b.nickname
|
||||
for b in UserBoss.query.filter(user_id__in=_boss_user_ids).only('user_id', 'nickname')
|
||||
}
|
||||
|
||||
# 构建返回数据
|
||||
paihang_list = []
|
||||
for index, guanshi in enumerate(guanshi_list):
|
||||
# 获取用户主表信息
|
||||
user_main = guanshi.user
|
||||
|
||||
# 管事的昵称需要查询老板扩展表
|
||||
guanshi_nicheng = _boss_nicheng_map.get(guanshi.user_id, '未设置昵称')
|
||||
|
||||
# 获取头像(相对URL)
|
||||
touxiang_url = user_main.Avatar or ''
|
||||
|
||||
# 获取数量
|
||||
if leixing == 1:
|
||||
chongzhi_shuliang = guanshi.jinrichongzhi
|
||||
else:
|
||||
chongzhi_shuliang = guanshi.jinyuechongzhi
|
||||
|
||||
paihang_list.append({
|
||||
'uid': user_main.UserUID,
|
||||
'touxiang': touxiang_url,
|
||||
'nicheng': guanshi_nicheng,
|
||||
'jine': float(chongzhi_shuliang), # 这里jine字段实际存储的是数量
|
||||
'mingci': index + 1
|
||||
})
|
||||
|
||||
return paihang_list, True
|
||||
|
||||
def chaxun_shangjia_paihang(self, leixing):
|
||||
"""
|
||||
查询商家排行榜
|
||||
:param leixing: 1=今日流水,2=今月流水
|
||||
:return: (排行榜列表, 是否展示标志)
|
||||
"""
|
||||
# 确定查询的字段
|
||||
if leixing == 1:
|
||||
# 今日流水
|
||||
liushui_ziduan = 'jinriliushui'
|
||||
else:
|
||||
# 今月流水
|
||||
liushui_ziduan = 'jinyueliushui'
|
||||
|
||||
# 查询条件:流水大于0,商家状态正常(状态1为正常)
|
||||
# 注意:商家表中的zhuangtai字段
|
||||
chaxun_tiaojian = Q(**{f'{liushui_ziduan}__gt': 0}) & Q(zhuangtai=1)
|
||||
|
||||
# 查询商家扩展表,并关联用户主表
|
||||
shangjia_list = UserShangjia.query.select_related('user').filter(
|
||||
chaxun_tiaojian
|
||||
).order_by(f'-{liushui_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
|
||||
|
||||
# 检查是否满足展示条件
|
||||
if len(shangjia_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
|
||||
# 人数不足,不展示
|
||||
return [], False
|
||||
|
||||
# 检查第一名金额是否达到最低要求
|
||||
if leixing == 1:
|
||||
di_yi_ming_liushui = shangjia_list[0].jinriliushui
|
||||
else:
|
||||
di_yi_ming_liushui = shangjia_list[0].jinyueliushui
|
||||
|
||||
if di_yi_ming_liushui < self.DI_YI_MING_ZUI_DI_JIN_E:
|
||||
# 第一名流水不足,不展示
|
||||
return [], False
|
||||
|
||||
# 批量预取老板扩展表昵称
|
||||
_boss_user_ids = [s.user_id for s in shangjia_list]
|
||||
_boss_nicheng_map = {
|
||||
b.user_id: b.nickname
|
||||
for b in UserBoss.query.filter(user_id__in=_boss_user_ids).only('user_id', 'nickname')
|
||||
}
|
||||
|
||||
# 构建返回数据
|
||||
paihang_list = []
|
||||
for index, shangjia in enumerate(shangjia_list):
|
||||
# 获取用户主表信息
|
||||
user_main = shangjia.user
|
||||
|
||||
# 商家的昵称需要查询老板扩展表
|
||||
shangjia_nicheng = _boss_nicheng_map.get(shangjia.user_id, '未设置昵称')
|
||||
|
||||
# 获取头像(相对URL)
|
||||
touxiang_url = user_main.Avatar or ''
|
||||
|
||||
# 获取流水金额
|
||||
if leixing == 1:
|
||||
liushui_jine = shangjia.jinriliushui
|
||||
else:
|
||||
liushui_jine = shangjia.jinyueliushui
|
||||
|
||||
paihang_list.append({
|
||||
'uid': user_main.UserUID,
|
||||
'touxiang': touxiang_url,
|
||||
'nicheng': shangjia_nicheng,
|
||||
'jine': float(liushui_jine),
|
||||
'mingci': index + 1
|
||||
})
|
||||
|
||||
return paihang_list, True
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class GetAdminZhanghaoView(APIView):
|
||||
"""
|
||||
获取管理员账号列表接口
|
||||
路径:/peizhi/adzhhq
|
||||
方法:POST
|
||||
权限:JWT Token + 管理员验证
|
||||
参数:
|
||||
- zhanghao: 当前登录的管理员账号(字符串)
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 🔥 1. 身份验证(100%按表模型)
|
||||
user = request.user
|
||||
|
||||
# 获取当前用户的手机号(CharField,字符串类型)
|
||||
current_phone = getattr(user, 'Phone', '')
|
||||
if not current_phone:
|
||||
return Response({
|
||||
'code': 401,
|
||||
'message': '用户手机号信息不完整',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 检查前端传递的账号是否匹配(都是字符串)
|
||||
frontend_zhanghao = request.data.get('zhanghao', '').strip()
|
||||
if frontend_zhanghao != current_phone:
|
||||
return Response({
|
||||
'code': 403,
|
||||
'message': '账号不匹配,无权限访问',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# 🔥 2. 验证是否为管理员(user_type是CharField,值是'admin'字符串)
|
||||
user_type = getattr(user, 'user_type', '')
|
||||
if user_type != 'admin':
|
||||
# 检查是否有管理员扩展表
|
||||
if not hasattr(user, 'AdminProfile'):
|
||||
return Response({
|
||||
'code': 403,
|
||||
'message': '无管理员权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# 🔥 3. 查询管理员账号列表(全部按字符串查询)
|
||||
with connection.cursor() as cursor:
|
||||
# 🔥 查询所有管理员账号(user_type='admin' 字符串)
|
||||
# 🔥 phone和yonghuid都是CharField,xiugaiid也是CharField
|
||||
sql = """
|
||||
SELECT
|
||||
um.id,
|
||||
um.phone,
|
||||
um.yonghuid,
|
||||
um.UserType,
|
||||
COALESCE(COUNT(xjl.id), 0) as jilushuliang
|
||||
FROM user_main um
|
||||
LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid
|
||||
WHERE um.UserType = 'admin'
|
||||
GROUP BY um.id, um.phone, um.yonghuid, um.UserType
|
||||
ORDER BY um.CreateTime DESC
|
||||
"""
|
||||
|
||||
cursor.execute(sql)
|
||||
results = cursor.fetchall()
|
||||
|
||||
# 🔥 4. 处理查询结果(全部保持字符串类型)
|
||||
zhanghao_list = []
|
||||
for row in results:
|
||||
zhanghao_list.append({
|
||||
#'id': row[0],
|
||||
'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
|
||||
'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
|
||||
'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串
|
||||
'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串
|
||||
'jilushuliang': int(row[4]) # 只有这个是数字
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'message': '获取成功',
|
||||
'data': zhanghao_list
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取管理员账号列表失败: {str(e)}")
|
||||
traceback.print_exc()
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '系统错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class GetXiugaiJiluView(APIView):
|
||||
"""
|
||||
获取修改记录列表接口
|
||||
路径:/peizhi/adhqxg
|
||||
方法:POST
|
||||
权限:JWT Token + 管理员验证
|
||||
参数:
|
||||
- zhanghao: 当前登录的管理员账号(字符串)
|
||||
- xiugaizhe_zhanghaoid: 选中的管理员账号ID(字符串,对应phone或yonghuid)
|
||||
- page: 页码(整数)
|
||||
- pagesize: 每页条数(整数)
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 🔥 1. 身份验证(100%按表模型)
|
||||
user = request.user
|
||||
|
||||
# 获取当前用户的手机号(CharField,字符串)
|
||||
current_phone = getattr(user, 'Phone', '')
|
||||
if not current_phone:
|
||||
return Response({
|
||||
'code': 401,
|
||||
'message': '用户手机号信息不完整',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 检查前端传递的账号是否匹配(字符串比较)
|
||||
frontend_zhanghao = request.data.get('zhanghao', '')
|
||||
if not isinstance(frontend_zhanghao, str):
|
||||
frontend_zhanghao = str(frontend_zhanghao)
|
||||
frontend_zhanghao = frontend_zhanghao.strip()
|
||||
|
||||
if frontend_zhanghao != current_phone:
|
||||
return Response({
|
||||
'code': 403,
|
||||
'message': '账号不匹配,无权限访问',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# 🔥 2. 验证是否为管理员(user_type='admin'字符串)
|
||||
user_type = getattr(user, 'user_type', '')
|
||||
if user_type != 'admin':
|
||||
if not hasattr(user, 'AdminProfile'):
|
||||
return Response({
|
||||
'code': 403,
|
||||
'message': '无管理员权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# 🔥 3. 获取参数(100%按实际类型)
|
||||
# 🔥 xiugaizhe_zhanghaoid 是字符串(对应xiugaiid字段,CharField)
|
||||
xiugaizhe_zhanghaoid_param = request.data.get('xiugaizhe_zhanghaoid', '')
|
||||
|
||||
|
||||
# 🔥 强制转为字符串(因为数据库是CharField)
|
||||
if xiugaizhe_zhanghaoid_param is None:
|
||||
xiugaizhe_zhanghaoid = ''
|
||||
else:
|
||||
# 无论如何都转为字符串
|
||||
xiugaizhe_zhanghaoid = str(xiugaizhe_zhanghaoid_param).strip()
|
||||
|
||||
|
||||
# 🔥 分页参数转为整数
|
||||
try:
|
||||
page = int(request.data.get('page', 1))
|
||||
except:
|
||||
page = 1
|
||||
|
||||
try:
|
||||
pagesize = int(request.data.get('pagesize', 10))
|
||||
except:
|
||||
pagesize = 10
|
||||
|
||||
# 🔥 4. 参数验证
|
||||
if not xiugaizhe_zhanghaoid:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '修改者账号ID不能为空',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if page < 1:
|
||||
page = 1
|
||||
if pagesize < 1 or pagesize > 50:
|
||||
pagesize = 10
|
||||
|
||||
# 🔥 5. 高效查询(100%按表字段类型)
|
||||
with connection.cursor() as cursor:
|
||||
# 🔥 5.1 查询总记录数(xiugaiid是CharField,用字符串查询)
|
||||
count_sql = """
|
||||
SELECT COUNT(*)
|
||||
FROM xiugaijilu
|
||||
WHERE xiugaiid = %s
|
||||
"""
|
||||
cursor.execute(count_sql, [xiugaizhe_zhanghaoid])
|
||||
total_count = cursor.fetchone()[0]
|
||||
|
||||
# 🔥 5.2 计算分页
|
||||
offset = (page - 1) * pagesize
|
||||
|
||||
# 🔥 5.3 查询当前页数据(使用参数化查询,防止SQL注入)
|
||||
data_sql = """
|
||||
SELECT
|
||||
xjl.id,
|
||||
xjl.yonghuid, -- CharField
|
||||
xjl.xiugaiid, -- CharField
|
||||
xjl.leixing, -- IntegerField
|
||||
xjl.xiugaitijiao, -- DecimalField
|
||||
xjl.xiugaitijiaoq, -- DecimalField
|
||||
xjl.jifen, -- IntegerField
|
||||
xjl.yjifen, -- IntegerField
|
||||
xjl.yajin, -- DecimalField
|
||||
xjl.yyajin, -- DecimalField
|
||||
xjl.shangjiayue, -- DecimalField
|
||||
xjl.shangjiayueq, -- DecimalField
|
||||
xjl.guanshiyue, -- DecimalField
|
||||
xjl.guanshiyueq, -- DecimalField
|
||||
xjl.huiyuants, -- IntegerField
|
||||
xjl.huiyuan_id, -- CharField
|
||||
xjl.qitashuoming, -- TextField
|
||||
xjl.CreateTime, -- DateTimeField
|
||||
um.avatar -- CharField
|
||||
FROM xiugaijilu xjl
|
||||
LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid
|
||||
WHERE xjl.xiugaiid = %s
|
||||
ORDER BY xjl.CreateTime DESC
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
|
||||
cursor.execute(data_sql, [xiugaizhe_zhanghaoid, pagesize, offset])
|
||||
records = cursor.fetchall()
|
||||
|
||||
# 🔥 6. 处理查询结果(严格按照字段类型处理)
|
||||
# 预查询修改者账号信息(循环外执行一次,避免循环内重复查询同一 SQL)
|
||||
xiugaizhe_zhanghao = ''
|
||||
with connection.cursor() as cursor2:
|
||||
cursor2.execute(
|
||||
"SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1",
|
||||
[xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid]
|
||||
)
|
||||
xiugaizhe_info = cursor2.fetchone()
|
||||
if xiugaizhe_info:
|
||||
xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \
|
||||
xiugaizhe_info[1] else xiugaizhe_zhanghaoid
|
||||
|
||||
jilu_list = []
|
||||
for record in records:
|
||||
# 修改者账号信息已预查询,直接使用
|
||||
|
||||
# 获取会员名称(如果有)
|
||||
huiyuan_mingzi = ''
|
||||
if record[15]: # huiyuan_id是CharField
|
||||
with connection.cursor() as cursor3:
|
||||
cursor3.execute(
|
||||
"SELECT jieshao FROM huiyuan WHERE huiyuan_id = %s LIMIT 1",
|
||||
[str(record[15])] # 🔥 转为字符串查询
|
||||
)
|
||||
huiyuan_info = cursor3.fetchone()
|
||||
if huiyuan_info:
|
||||
huiyuan_mingzi = str(huiyuan_info[0]) if huiyuan_info[0] else ''
|
||||
|
||||
# 🔥 严格按照前端字段名映射,保持正确类型
|
||||
jilu_data = {
|
||||
# 基础字段(字符串)
|
||||
'id': record[0],
|
||||
'beixiugai_yonghuid': str(record[1]) if record[1] else '', # yonghuid
|
||||
'xiugaizhe_id': str(record[2]) if record[2] else '', # xiugaiid
|
||||
'xiugaizhe_zhanghao': xiugaizhe_zhanghao, # 字符串
|
||||
'beixiugai_yonghuleixing': record[3], # leixing是整数
|
||||
|
||||
# 打手字段(Decimal转float,Integer转int)
|
||||
'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00,
|
||||
'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00,
|
||||
'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0,
|
||||
'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0,
|
||||
'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00,
|
||||
'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00,
|
||||
|
||||
# 商家字段
|
||||
'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00,
|
||||
'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00,
|
||||
|
||||
# 管事字段
|
||||
'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00,
|
||||
'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00,
|
||||
|
||||
# 会员字段
|
||||
'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0,
|
||||
'huiyuan_id': str(record[15]) if record[15] else '', # 字符串
|
||||
'huiyuan_mingzi': huiyuan_mingzi, # 字符串
|
||||
|
||||
# 其他字段
|
||||
'qita_shuoming': str(record[16]) if record[16] else '', # 字符串
|
||||
'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None,
|
||||
'touxiang': str(record[18]) if record[18] else '' # avatar是字符串
|
||||
}
|
||||
|
||||
jilu_list.append(jilu_data)
|
||||
|
||||
# 🔥 7. 分页控制
|
||||
queried_records = len(jilu_list)
|
||||
has_more = False
|
||||
|
||||
if queried_records == pagesize:
|
||||
total_queried = (page - 1) * pagesize + queried_records
|
||||
has_more = total_queried < total_count
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'message': '获取成功',
|
||||
'data': {
|
||||
'list': jilu_list,
|
||||
'zongshuliang': total_count,
|
||||
'dangqian_page': page,
|
||||
'page_size': pagesize,
|
||||
'has_more': has_more,
|
||||
'shijichaxundediaoshuliang': queried_records
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except ValueError as e:
|
||||
print(f"参数错误: {str(e)}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '参数错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取修改记录失败: {str(e)}")
|
||||
traceback.print_exc()
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '系统错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1032
config/views/merchant_links.py
Normal file
1032
config/views/merchant_links.py
Normal file
File diff suppressed because it is too large
Load Diff
490
config/views/merchant_templates.py
Normal file
490
config/views/merchant_templates.py
Normal file
@@ -0,0 +1,490 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
import secrets
|
||||
import hashlib
|
||||
import threading
|
||||
import urllib.parse
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Q, F, Max, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.core.cache import cache
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils import timezone
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||
|
||||
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||
# from utils.ip_security import *
|
||||
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||
|
||||
from backend.utils import update_shangjia_daily
|
||||
from utils.chat_utils import subscribe_merchant_link_chat
|
||||
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||
|
||||
from ..models import (
|
||||
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||
MiniappScriptScene, MiniappScriptAutoReply,
|
||||
)
|
||||
|
||||
from users.models import (
|
||||
AdminProfile, UserDashou, UserBoss,
|
||||
UserShangjia, UserGuanshi, UserZuzhang
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
|
||||
from rank.models import Chenghao, DingdanBiaoqian
|
||||
|
||||
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||
|
||||
from ..serializers import PopupConfigSerializer
|
||||
import traceback
|
||||
import requests
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ShangjiaMobanListView(APIView):
|
||||
"""
|
||||
商家订单模板列表接口
|
||||
POST /peizhi/sjddmblb
|
||||
权限: JWT认证
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user = request.user
|
||||
yonghu_id = user.UserUID
|
||||
|
||||
leixing_id = request.data.get('shangpinTypeId')
|
||||
if not leixing_id:
|
||||
return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400)
|
||||
|
||||
get_type = int(request.data.get('getType', 2))
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('pageSize', 50))
|
||||
|
||||
keyword = request.data.get('keyword', '')
|
||||
min_price = request.data.get('minPrice', '')
|
||||
label_id = request.data.get('labelId', None)
|
||||
if label_id is not None:
|
||||
try:
|
||||
label_id = int(label_id)
|
||||
except (ValueError, TypeError):
|
||||
label_id = None
|
||||
|
||||
query = Q(UserID=yonghu_id, ProductTypeID=leixing_id)
|
||||
|
||||
if get_type == 1: # 搜索模式
|
||||
if keyword:
|
||||
keyword = str(keyword).strip()
|
||||
try:
|
||||
mid = int(keyword)
|
||||
query &= Q(id=mid) | Q(TemplateDesc__icontains=keyword)
|
||||
except ValueError:
|
||||
query &= Q(TemplateDesc__icontains=keyword)
|
||||
if min_price:
|
||||
try:
|
||||
query &= Q(Price__gte=Decimal(str(min_price)))
|
||||
except:
|
||||
pass
|
||||
if label_id:
|
||||
query &= Q(TitleID=label_id)
|
||||
|
||||
mobans = ShangjiaMoban.query.filter(query).order_by('-id')
|
||||
formatted = []
|
||||
for moban in mobans:
|
||||
texiao_json = ''
|
||||
label_name = ''
|
||||
if moban.TitleID:
|
||||
try:
|
||||
ch = Chenghao.query.get(id=moban.TitleID)
|
||||
label_name = ch.mingcheng
|
||||
texiao_json = ch.texiao_miaoshu or ''
|
||||
except Chenghao.DoesNotExist:
|
||||
pass
|
||||
formatted.append({
|
||||
'mobanId': moban.id,
|
||||
'shangpinTypeId': moban.ProductTypeID,
|
||||
'jieshao': moban.TemplateDesc,
|
||||
'jiage': str(moban.Price),
|
||||
'fabushuliang': moban.PublishedCount,
|
||||
'commissionEnabled': moban.CommissionGrabEnabled,
|
||||
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
|
||||
'labelId': moban.TitleID,
|
||||
'labelName': label_name,
|
||||
'texiaoJson': texiao_json,
|
||||
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else ''
|
||||
})
|
||||
return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}})
|
||||
|
||||
# 分页模式
|
||||
queryset = ShangjiaMoban.query.filter(query).order_by('-id')
|
||||
paginator = Paginator(queryset, page_size)
|
||||
if page < 1 or page > paginator.num_pages:
|
||||
return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
|
||||
|
||||
current_page = paginator.page(page)
|
||||
# 批量预取所有称号,避免循环内 N+1
|
||||
_mobans_list = list(current_page.object_list)
|
||||
_title_ids = [m.TitleID for m in _mobans_list if m.TitleID]
|
||||
_ch_map = {
|
||||
ch.id: ch
|
||||
for ch in Chenghao.query.filter(id__in=_title_ids).only('id', 'mingcheng', 'texiao_miaoshu')
|
||||
} if _title_ids else {}
|
||||
formatted = []
|
||||
for moban in _mobans_list:
|
||||
texiao_json = ''
|
||||
label_name = ''
|
||||
ch = _ch_map.get(moban.TitleID)
|
||||
if ch:
|
||||
label_name = ch.mingcheng
|
||||
texiao_json = ch.texiao_miaoshu or ''
|
||||
formatted.append({
|
||||
'mobanId': moban.id,
|
||||
'shangpinTypeId': moban.ProductTypeID,
|
||||
'jieshao': moban.TemplateDesc,
|
||||
'jiage': str(moban.Price),
|
||||
'fabushuliang': moban.PublishedCount,
|
||||
'commissionEnabled': moban.CommissionGrabEnabled,
|
||||
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
|
||||
'labelId': moban.TitleID,
|
||||
'labelName': label_name,
|
||||
'texiaoJson': texiao_json,
|
||||
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else ''
|
||||
})
|
||||
|
||||
has_more = current_page.has_next()
|
||||
if len(formatted) < page_size:
|
||||
has_more = False
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'msg': '获取成功',
|
||||
'data': {
|
||||
'list': formatted,
|
||||
'total': paginator.count,
|
||||
'hasMore': has_more,
|
||||
'currentPage': page,
|
||||
'pageSize': page_size,
|
||||
'totalPages': paginator.num_pages
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取模板列表失败: {str(e)}", exc_info=True)
|
||||
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ShangjiaTianjiaMobanView(APIView):
|
||||
"""
|
||||
商家添加订单模板接口
|
||||
POST /peizhi/sjtjddmb
|
||||
权限: JWT认证
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 获取当前商家用户ID
|
||||
user = request.user
|
||||
yonghu_id = user.UserUID
|
||||
|
||||
# 2. 接收并清洗参数,避免类型错误
|
||||
leixing_id = request.data.get('shangpinTypeId')
|
||||
jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else ''
|
||||
jiage = request.data.get('jiage', '')
|
||||
# 佣金相关
|
||||
commission_enabled = request.data.get('commissionEnabled', False)
|
||||
commission_value = request.data.get('commissionValue', None)
|
||||
# 标签ID(称号ID)
|
||||
label_id = request.data.get('labelId', None)
|
||||
|
||||
# 3. 必填验证
|
||||
if not leixing_id or not jieshao or not jiage:
|
||||
logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'msg': '缺少必填字段(订单类型、介绍、价格)',
|
||||
'data': {}
|
||||
}, status=400)
|
||||
|
||||
# 4. 价格格式验证
|
||||
try:
|
||||
jiage_decimal = Decimal(str(jiage))
|
||||
if jiage_decimal <= 0:
|
||||
return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400)
|
||||
if jiage_decimal > 10000:
|
||||
return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400)
|
||||
except (InvalidOperation, ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
|
||||
|
||||
# 5. 介绍长度验证
|
||||
if len(jieshao) > 100:
|
||||
return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
|
||||
|
||||
# 6. 佣金验证(若启用)
|
||||
CommissionGrabEnabled = False
|
||||
CommissionPrice = None
|
||||
if commission_enabled:
|
||||
CommissionGrabEnabled = True
|
||||
if not commission_value:
|
||||
return Response({'code': 400, 'msg': '开启佣金时,佣金金额不能为空', 'data': {}}, status=400)
|
||||
try:
|
||||
comm_decimal = Decimal(str(commission_value))
|
||||
if comm_decimal <= 0:
|
||||
return Response({'code': 400, 'msg': '佣金必须大于0', 'data': {}}, status=400)
|
||||
CommissionPrice = comm_decimal
|
||||
except (InvalidOperation, ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
|
||||
|
||||
# 7. 标签ID清洗(前端可能传数字或字符串)
|
||||
cleaned_label_id = None
|
||||
if label_id is not None and str(label_id).strip():
|
||||
try:
|
||||
cleaned_label_id = int(label_id)
|
||||
except (ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400)
|
||||
|
||||
# 8. 验证商品类型是否存在且可用
|
||||
try:
|
||||
shangpin_obj = ShangpinLeixing.query.get(
|
||||
id=leixing_id,
|
||||
shenhezhuangtai=1 # 审核通过
|
||||
)
|
||||
except ShangpinLeixing.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404)
|
||||
|
||||
# 9. 检查同一用户、同一类型下是否已存在相同介绍的模板
|
||||
if ShangjiaMoban.query.filter(
|
||||
UserID=yonghu_id,
|
||||
ProductTypeID=leixing_id,
|
||||
TemplateDesc=jieshao
|
||||
).exists():
|
||||
return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
|
||||
|
||||
# 10. 执行数据库创建(事务)
|
||||
with transaction.atomic():
|
||||
moban = ShangjiaMoban.query.create(
|
||||
UserID=yonghu_id,
|
||||
ProductTypeID=leixing_id,
|
||||
TemplateDesc=jieshao,
|
||||
Price=jiage_decimal,
|
||||
PublishedCount=0,
|
||||
CommissionGrabEnabled=CommissionGrabEnabled,
|
||||
CommissionPrice=CommissionPrice,
|
||||
TitleID=cleaned_label_id
|
||||
)
|
||||
|
||||
# 11. 返回成功数据(字段与前端对齐)
|
||||
return Response({
|
||||
'code': 200,
|
||||
'msg': '模板创建成功',
|
||||
'data': {
|
||||
'mobanId': moban.id,
|
||||
'shangpinTypeId': leixing_id,
|
||||
'jieshao': moban.TemplateDesc,
|
||||
'jiage': str(moban.Price),
|
||||
'fabushuliang': moban.PublishedCount,
|
||||
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'commissionEnabled': moban.CommissionGrabEnabled,
|
||||
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
|
||||
'labelId': moban.TitleID
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
# 打印完整错误堆栈到日志
|
||||
logger.error(f"添加模板失败: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'msg': '服务器内部错误,添加模板失败,请联系管理员',
|
||||
'data': {}
|
||||
}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
class ShangjiaShanchuMobanView(APIView):
|
||||
"""
|
||||
删除模板
|
||||
POST /peizhi/sjddmbsc
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user = request.user
|
||||
yonghu_id = user.UserUID
|
||||
|
||||
moban_id = request.data.get('mobanId')
|
||||
leixing_id = request.data.get('shangpinTypeId')
|
||||
|
||||
if not all([moban_id, leixing_id]):
|
||||
return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400)
|
||||
|
||||
try:
|
||||
moban = ShangjiaMoban.query.get(id=moban_id, UserID=yonghu_id, ProductTypeID=leixing_id)
|
||||
except ShangjiaMoban.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404)
|
||||
|
||||
# 强制硬删除
|
||||
moban.delete()
|
||||
|
||||
return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}})
|
||||
except Exception as e:
|
||||
logger.error(f"删除模板失败: {str(e)}", exc_info=True)
|
||||
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ShangjiaGengxinMobanView(APIView):
|
||||
"""
|
||||
商家更新订单模板接口
|
||||
POST /peizhi/sjddmbgx
|
||||
权限: JWT认证
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user = request.user
|
||||
yonghu_id = user.UserUID
|
||||
|
||||
moban_id = request.data.get('mobanId')
|
||||
leixing_id = request.data.get('shangpinTypeId')
|
||||
new_jieshao = request.data.get('jieshao', '').strip()
|
||||
new_jiage = request.data.get('jiage', '').strip()
|
||||
# 新增字段
|
||||
commission_enabled = request.data.get('commissionEnabled')
|
||||
commission_value = request.data.get('commissionValue')
|
||||
label_id = request.data.get('labelId')
|
||||
|
||||
if not all([moban_id, leixing_id]):
|
||||
return Response({'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {}}, status=400)
|
||||
|
||||
# 查询模板
|
||||
try:
|
||||
moban = ShangjiaMoban.query.get(
|
||||
id=moban_id,
|
||||
UserID=yonghu_id,
|
||||
ProductTypeID=leixing_id
|
||||
)
|
||||
except ShangjiaMoban.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '模板不存在或无权修改', 'data': {}}, status=404)
|
||||
|
||||
update_fields = []
|
||||
|
||||
# 修改介绍
|
||||
if new_jieshao:
|
||||
if len(new_jieshao) > 100:
|
||||
return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
|
||||
if new_jieshao != moban.TemplateDesc:
|
||||
moban.TemplateDesc = new_jieshao
|
||||
update_fields.append('TemplateDesc')
|
||||
|
||||
# 修改价格
|
||||
if new_jiage:
|
||||
try:
|
||||
jiage_decimal = Decimal(new_jiage)
|
||||
if jiage_decimal <= 0 or jiage_decimal > 10000:
|
||||
return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400)
|
||||
if jiage_decimal != moban.Price:
|
||||
moban.Price = jiage_decimal
|
||||
update_fields.append('Price')
|
||||
except (InvalidOperation, ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
|
||||
|
||||
# 修改标签
|
||||
if label_id is not None: # 允许传空字符串或不传,传0表示清空
|
||||
new_label = int(label_id) if label_id else None
|
||||
if new_label != moban.TitleID:
|
||||
moban.TitleID = new_label
|
||||
update_fields.append('TitleID')
|
||||
|
||||
# 修改佣金设置
|
||||
if commission_enabled is not None:
|
||||
enabled = bool(commission_enabled)
|
||||
if enabled:
|
||||
if not commission_value:
|
||||
return Response({'code': 400, 'msg': '佣金价格不能为空', 'data': {}}, status=400)
|
||||
try:
|
||||
comm_decimal = Decimal(commission_value)
|
||||
if comm_decimal < 0.1 or comm_decimal > 50:
|
||||
return Response({'code': 400, 'msg': '佣金需在0.1-50元之间', 'data': {}}, status=400)
|
||||
if moban.CommissionGrabEnabled != True or moban.CommissionPrice != comm_decimal:
|
||||
moban.CommissionGrabEnabled = True
|
||||
moban.CommissionPrice = comm_decimal
|
||||
update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice'])
|
||||
except (InvalidOperation, ValueError, TypeError):
|
||||
return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
|
||||
else:
|
||||
if moban.CommissionGrabEnabled != False or moban.CommissionPrice is not None:
|
||||
moban.CommissionGrabEnabled = False
|
||||
moban.CommissionPrice = None
|
||||
update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice'])
|
||||
|
||||
if not update_fields:
|
||||
return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400)
|
||||
|
||||
# 检查介绍唯一性
|
||||
if 'TemplateDesc' in update_fields:
|
||||
if ShangjiaMoban.query.filter(
|
||||
UserID=yonghu_id,
|
||||
ProductTypeID=leixing_id,
|
||||
TemplateDesc=moban.TemplateDesc
|
||||
).exclude(id=moban_id).exists():
|
||||
return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
|
||||
|
||||
with transaction.atomic():
|
||||
moban.save(update_fields=update_fields)
|
||||
|
||||
# 返回最新数据
|
||||
return Response({
|
||||
'code': 200,
|
||||
'msg': '模板修改成功',
|
||||
'data': {
|
||||
'mobanId': moban.id,
|
||||
'shangpinTypeId': moban.ProductTypeID,
|
||||
'jieshao': moban.TemplateDesc,
|
||||
'jiage': str(moban.Price),
|
||||
'fabushuliang': moban.PublishedCount,
|
||||
'commissionEnabled': moban.CommissionGrabEnabled,
|
||||
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice else None,
|
||||
'labelId': moban.TitleID,
|
||||
'updateTime': moban.UpdateTime.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"修改商家模板失败: {str(e)}", exc_info=True)
|
||||
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
334
config/views/miniapp_config.py
Normal file
334
config/views/miniapp_config.py
Normal file
@@ -0,0 +1,334 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
import secrets
|
||||
import hashlib
|
||||
import threading
|
||||
import urllib.parse
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Q, F, Max, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.core.cache import cache
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils import timezone
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||
|
||||
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||
# from utils.ip_security import *
|
||||
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||
|
||||
from backend.utils import update_shangjia_daily
|
||||
from utils.chat_utils import subscribe_merchant_link_chat
|
||||
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||
|
||||
from ..models import (
|
||||
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||
MiniappScriptScene, MiniappScriptAutoReply,
|
||||
)
|
||||
|
||||
from users.models import (
|
||||
AdminProfile, UserDashou, UserBoss,
|
||||
UserShangjia, UserGuanshi, UserZuzhang
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
|
||||
from rank.models import Chenghao, DingdanBiaoqian
|
||||
|
||||
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||
|
||||
from ..serializers import PopupConfigSerializer
|
||||
import traceback
|
||||
import requests
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GetDynamicConfigView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
from jituan.services.display_config import get_tupianpeizhi_url
|
||||
|
||||
from jituan.services.miniapp_assets import build_miniapp_assets_payload
|
||||
|
||||
cos_oss_url = getattr(settings, 'COS_DOMAIN', '')
|
||||
if cos_oss_url and not cos_oss_url.endswith('/'):
|
||||
cos_oss_url += '/'
|
||||
cos_bucket = getattr(settings, 'COS_BUCKET', '')
|
||||
cos_region = getattr(settings, 'COS_REGION', 'ap-shanghai')
|
||||
goeasy_host = 'hangzhou.goeasy.io'
|
||||
goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '')
|
||||
kefu_link = getattr(settings, 'KEFU_LINK', '')
|
||||
kefu_enterprise_id = getattr(settings, 'KEFU_ENTERPRISE_ID', '')
|
||||
rule_img = get_tupianpeizhi_url(request, 1)
|
||||
avatar_img = get_tupianpeizhi_url(request, 2)
|
||||
morentouxiang = avatar_img or 'beijing/morentouxiang.jpg'
|
||||
dashouguize = rule_img or 'a_long/dashouguize.jpg'
|
||||
|
||||
# 构造返回数据
|
||||
data = {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"cos": {
|
||||
"bucket": cos_bucket,
|
||||
"region": cos_region,
|
||||
"ossImageUrl": cos_oss_url,
|
||||
"uploadPathPrefix": "order/"
|
||||
},
|
||||
"goEasy": {
|
||||
"host": goeasy_host,
|
||||
"appkey": goeasy_appkey
|
||||
},
|
||||
"otherConfig": {
|
||||
"morentouxiang": morentouxiang,
|
||||
"dashouguize": dashouguize
|
||||
},
|
||||
"kefu": {
|
||||
"link": kefu_link,
|
||||
"enterpriseId": kefu_enterprise_id
|
||||
},
|
||||
"miniappAssets": build_miniapp_assets_payload(request),
|
||||
}
|
||||
}
|
||||
return Response(data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取动态配置失败: {str(e)}", exc_info=True)
|
||||
return Response({"code": 500, "msg": "服务器内部错误"}, status=500)
|
||||
|
||||
|
||||
class HaibaoPeizhiView(APIView):
|
||||
"""小程序推广海报背景配置(按俱乐部)。"""
|
||||
permission_classes = [AllowAny]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
from jituan.services.display_config import _display_club_id
|
||||
from jituan.services.miniapp_assets import get_poster_payload
|
||||
|
||||
club_id = _display_club_id(request)
|
||||
poster = get_poster_payload(club_id)
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': {
|
||||
'club_id': club_id,
|
||||
'poster': poster,
|
||||
},
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error('获取海报配置失败: %s', e, exc_info=True)
|
||||
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
class PopupConfigView(APIView):
|
||||
"""
|
||||
获取指定页面的弹窗配置
|
||||
请求格式:POST { "pageKey": "dashouzhongxin" }
|
||||
返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } }
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
# 获取参数 pageKey
|
||||
page_key = request.data.get('pageKey')
|
||||
if not page_key:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'msg': '缺少参数 pageKey'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 查询页面配置(按俱乐部)
|
||||
from jituan.services.display_config import get_gonggao_content
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
try:
|
||||
popup_page = PopupPage.query.get(club_id=club_id, page_key=page_key, is_active=True)
|
||||
except PopupPage.DoesNotExist:
|
||||
# 该页面没有配置弹窗,返回空
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'serverTime': timezone.now().isoformat(),
|
||||
'popups': []
|
||||
}
|
||||
})
|
||||
|
||||
# 4. 查询该页面下所有启用的弹窗(且在有效时间范围内)
|
||||
now = timezone.now()
|
||||
popups = PopupConfig.query.filter(
|
||||
page=popup_page,
|
||||
is_active=True,
|
||||
start_time__lte=now,
|
||||
end_time__gte=now
|
||||
).prefetch_related('images') # 预加载图片,避免N+1查询
|
||||
# 按 sort_order 排序(model Meta 已定义)
|
||||
|
||||
# 5. 序列化
|
||||
serializer = PopupConfigSerializer(popups, many=True)
|
||||
|
||||
# 6. 返回数据
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'serverTime': now.isoformat(),
|
||||
'ignore_user_mute': popup_page.ignore_user_mute, # 新增
|
||||
'popups': serializer.data
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
class GetWithdrawModeView(APIView):
|
||||
"""
|
||||
获取提现模式配置
|
||||
前端请求 /peizhi/hqtxzsym
|
||||
返回格式: { "code": 0, "data": { "mode": 1 or 2 } }
|
||||
"""
|
||||
permission_classes = [IsAuthenticated] # JWT 认证
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
from jituan.services.club_user import get_user_club_id
|
||||
from jituan.services.withdraw_config import get_withdraw_mode
|
||||
|
||||
user = getattr(request, 'user', None)
|
||||
club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None
|
||||
if not club_id:
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
mode = get_withdraw_mode(club_id)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'mode': mode,
|
||||
'club_id': club_id,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class CheckPhoneAuthView(APIView):
|
||||
"""
|
||||
判断当前登录用户是否需要强制手机号认证
|
||||
|
||||
规则:
|
||||
- 已认证过手机号(PhoneVerified=True) → 不需要
|
||||
- 未认证时,满足以下任一条件 → 需要认证:
|
||||
打手:有会员购买记录,或押金 > 0
|
||||
商家:已是商家(存在商家扩展表)
|
||||
管事:已是管事(存在管事扩展表)
|
||||
组长:已是组长(存在组长扩展表)
|
||||
老板:有下单记录(alldingdan > 0)
|
||||
子客服:已是有效商家客服(merchant_staff_member 活跃)
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@staticmethod
|
||||
def _phone_auth_completed(user):
|
||||
"""是否已完成手机号认证(兼容 PhoneVerified 未写入的老数据)。"""
|
||||
if bool(getattr(user, 'PhoneVerified', False)):
|
||||
return True
|
||||
phone = (getattr(user, 'Phone', None) or '').strip()
|
||||
return len(phone) >= 11
|
||||
|
||||
def post(self, request):
|
||||
uid = getattr(request.user, 'UserUID', None)
|
||||
user = request.user
|
||||
if uid:
|
||||
try:
|
||||
user = User.query.get(UserUID=uid)
|
||||
except User.DoesNotExist:
|
||||
user = request.user
|
||||
|
||||
if self._phone_auth_completed(user):
|
||||
return Response({'code': 0, 'need_auth': False})
|
||||
|
||||
if self._dashou_meets_threshold(user):
|
||||
return Response({'code': 0, 'need_auth': True})
|
||||
if self._shangjia_meets_threshold(user):
|
||||
return Response({'code': 0, 'need_auth': True})
|
||||
if self._guanshi_meets_threshold(user):
|
||||
return Response({'code': 0, 'need_auth': True})
|
||||
if self._zuzhang_meets_threshold(user):
|
||||
return Response({'code': 0, 'need_auth': True})
|
||||
if self._boss_meets_threshold(user):
|
||||
return Response({'code': 0, 'need_auth': True})
|
||||
if self._staff_meets_threshold(user):
|
||||
return Response({'code': 0, 'need_auth': True})
|
||||
|
||||
return Response({'code': 0, 'need_auth': False})
|
||||
|
||||
def _staff_meets_threshold(self, user):
|
||||
"""子客服:已是有效商家客服"""
|
||||
try:
|
||||
from merchant_ops.services.authz import get_active_staff_member
|
||||
return get_active_staff_member(user) is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _dashou_meets_threshold(self, user):
|
||||
"""打手:有会员记录、押金 > 0、或有过接单/成交订单"""
|
||||
try:
|
||||
dashou = UserDashou.query.get(user=user)
|
||||
except UserDashou.DoesNotExist:
|
||||
return False
|
||||
if dashou.yajin and dashou.yajin > 0:
|
||||
return True
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
club_id = resolve_effective_club_id(self.request, user)
|
||||
if Huiyuangoumai.query.filter(yonghu_id=user.UserUID, club_id=club_id).exists():
|
||||
return True
|
||||
if (dashou.jiedanzongliang or 0) > 0 or (dashou.chengjiaozongliang or 0) > 0:
|
||||
return True
|
||||
return Order.query.filter(PlayerID=user.UserUID).exists()
|
||||
|
||||
def _shangjia_meets_threshold(self, user):
|
||||
"""商家:已是商家身份"""
|
||||
return UserShangjia.query.filter(user=user).exists()
|
||||
|
||||
def _guanshi_meets_threshold(self, user):
|
||||
"""管事:已是管事身份"""
|
||||
return UserGuanshi.query.filter(user=user).exists()
|
||||
|
||||
def _zuzhang_meets_threshold(self, user):
|
||||
"""组长:已是组长身份"""
|
||||
return UserZuzhang.query.filter(user=user).exists()
|
||||
|
||||
def _boss_meets_threshold(self, user):
|
||||
"""老板:有下单记录"""
|
||||
try:
|
||||
boss = UserBoss.query.get(user=user)
|
||||
except UserBoss.DoesNotExist:
|
||||
return False
|
||||
return (boss.alldingdan or 0) > 0
|
||||
|
||||
|
||||
553
config/views/qrcode_poster.py
Normal file
553
config/views/qrcode_poster.py
Normal file
@@ -0,0 +1,553 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
import secrets
|
||||
import hashlib
|
||||
import threading
|
||||
import urllib.parse
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Q, F, Max, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.core.cache import cache
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils import timezone
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||
|
||||
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||
# from utils.ip_security import *
|
||||
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||
|
||||
from backend.utils import update_shangjia_daily
|
||||
from utils.chat_utils import subscribe_merchant_link_chat
|
||||
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||
|
||||
from ..models import (
|
||||
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||
MiniappScriptScene, MiniappScriptAutoReply,
|
||||
)
|
||||
|
||||
from users.models import (
|
||||
AdminProfile, UserDashou, UserBoss,
|
||||
UserShangjia, UserGuanshi, UserZuzhang
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
|
||||
from rank.models import Chenghao, DingdanBiaoqian
|
||||
|
||||
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||
|
||||
from ..serializers import PopupConfigSerializer
|
||||
import traceback
|
||||
import requests
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuanZhuAListView(APIView):
|
||||
"""
|
||||
获取关注阿龙页面图片列表
|
||||
POST /peizhi/gzal
|
||||
需要JWT认证
|
||||
返回:
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"images": ["相对URL1", "相对URL2", ...],
|
||||
"last_update": "2025-03-16 12:34:56",
|
||||
"visit_count": 123
|
||||
}
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
|
||||
club_id = resolve_effective_club_id(request, request.user)
|
||||
qs = Tupianpeizhi.query.filter(ImageType=3, club_id=club_id)
|
||||
if not qs.exists():
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': {
|
||||
'images': [],
|
||||
'last_update': None,
|
||||
'visit_count': 0
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
# 获取所有图片的相对URL
|
||||
images = list(qs.values_list('ImageURL', flat=True))
|
||||
|
||||
# 获取最新更新时间(取所有记录的UpdateTime最大值)
|
||||
last_update = qs.aggregate(UpdateTime__max=Max('UpdateTime'))['UpdateTime__max']
|
||||
last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None
|
||||
|
||||
# 原子性增加访问次数
|
||||
# 先更新所有记录,再获取更新后的最大次数
|
||||
qs.update(AccessCount=F('AccessCount') + 1)
|
||||
visit_count = qs.aggregate(access_count__max=Max('AccessCount'))['access_count__max'] or 0
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': {
|
||||
'images': images,
|
||||
'last_update': last_update_str,
|
||||
'visit_count': visit_count
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True)
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '服务器内部错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#不用缓存,一天2000次
|
||||
class GuanshiQRCodeView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
logger.info("===== 开始生成管事二维码 =====")
|
||||
current_user = request.user
|
||||
logger.info(f"当前用户ID: {current_user.UserUID}")
|
||||
|
||||
try:
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
from utils.weixin_token import get_club_mini_access_token, is_weixin_token_invalid
|
||||
|
||||
club_id = resolve_effective_club_id(request, current_user)
|
||||
try:
|
||||
guanshi = current_user.GuanshiProfile
|
||||
logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}")
|
||||
except UserGuanshi.DoesNotExist:
|
||||
logger.warning(f"用户 {current_user.UserUID} 不是管事身份")
|
||||
return Response(
|
||||
{'code': 403, 'message': '当前用户不是管事身份', 'data': None},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
if guanshi.zhuangtai != 1:
|
||||
logger.warning(f"用户 {current_user.UserUID} 管事状态异常: {guanshi.zhuangtai}")
|
||||
return Response(
|
||||
{'code': 403, 'message': '管事账号状态异常', 'data': None},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
invite_code = guanshi.yaoqingma
|
||||
if not invite_code:
|
||||
logger.info(f"用户 {current_user.UserUID} 无邀请码,开始生成")
|
||||
invite_code = CreateInvitationCode(str(current_user.UserUID))
|
||||
if not VerifyInvitationCode(invite_code):
|
||||
logger.error(f"邀请码唯一性校验失败,用户: {current_user.UserUID}")
|
||||
return Response(
|
||||
{'code': 500, 'message': '邀请码生成失败', 'data': None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
guanshi.yaoqingma = invite_code
|
||||
guanshi.save(update_fields=['yaoqingma'])
|
||||
logger.info(f"邀请码生成成功: {invite_code}")
|
||||
else:
|
||||
logger.info(f"使用已有邀请码: {invite_code}")
|
||||
|
||||
# 按俱乐部使用对应小程序 appid 的 access_token
|
||||
access_token = get_club_mini_access_token(club_id)
|
||||
if not access_token:
|
||||
logger.error("获取 access_token 失败 club_id=%s", club_id)
|
||||
return Response(
|
||||
{'code': 500, 'message': '获取微信access_token失败', 'data': None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
logger.info(f"获取到 access_token (前10位): {access_token[:10]}... club={club_id}")
|
||||
|
||||
encoded_invite = urllib.parse.quote(invite_code, safe='')
|
||||
# 新小程序:打手端 Tab「我的」;旧码 dashouduan 由小程序兼容页转发
|
||||
page_path = f'pages/fighter/fighter?inviteCode={encoded_invite}'
|
||||
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
|
||||
post_data = {
|
||||
'path': page_path,
|
||||
'width': 430,
|
||||
'auto_color': False,
|
||||
'line_color': {'r': 0, 'g': 0, 'b': 0}
|
||||
}
|
||||
logger.info(f"调用微信接口生成小程序码,path: {page_path}")
|
||||
|
||||
def _request_qr(token):
|
||||
try:
|
||||
return requests.post(
|
||||
f'https://api.weixin.qq.com/wxa/getwxacode?access_token={token}',
|
||||
json=post_data, timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"请求微信接口异常: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
wx_resp = _request_qr(access_token)
|
||||
if wx_resp is None:
|
||||
return Response(
|
||||
{'code': 500, 'message': '调用微信服务失败', 'data': None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
if wx_resp.headers.get('content-type', '').startswith('application/json'):
|
||||
try:
|
||||
err = wx_resp.json()
|
||||
if is_weixin_token_invalid(err.get('errcode'), err.get('errmsg', '')):
|
||||
access_token = get_club_mini_access_token(club_id, force_refresh=True)
|
||||
if access_token:
|
||||
wx_resp = _request_qr(access_token)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(f"微信接口响应状态码: {wx_resp.status_code}")
|
||||
|
||||
if wx_resp.status_code != 200:
|
||||
error_info = {}
|
||||
if wx_resp.headers.get('content-type') == 'application/json':
|
||||
try:
|
||||
error_info = wx_resp.json()
|
||||
except:
|
||||
pass
|
||||
errmsg = error_info.get('errmsg', '未知错误')
|
||||
logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
|
||||
if 'errcode' in error_info:
|
||||
logger.error(f"微信错误码: {error_info.get('errcode')}")
|
||||
return Response(
|
||||
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
content_type = wx_resp.headers.get('content-type', '')
|
||||
if 'image' not in content_type:
|
||||
try:
|
||||
error_info = wx_resp.json()
|
||||
errmsg = error_info.get('errmsg', '未知错误')
|
||||
logger.error(f"微信接口返回非图片内容: {errmsg}")
|
||||
return Response(
|
||||
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
except:
|
||||
logger.error("微信接口返回未知内容,无法解析为JSON")
|
||||
return Response(
|
||||
{'code': 500, 'message': '生成二维码失败', 'data': None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
timestamp = int(time.time())
|
||||
filename = f"{current_user.UserUID}_{timestamp}.png"
|
||||
oss_path = f"erweima/guanshi/{club_id}/{filename}"
|
||||
file_obj = io.BytesIO(wx_resp.content)
|
||||
logger.info(f"开始上传图片到COS: {oss_path}")
|
||||
|
||||
try:
|
||||
full_url = upload_to_oss(file_obj, oss_path)
|
||||
logger.info(f"上传成功,完整URL: {full_url}")
|
||||
except Exception as e:
|
||||
logger.error(f"上传到COS异常: {str(e)}", exc_info=True)
|
||||
return Response(
|
||||
{'code': 500, 'message': '图片上传失败', 'data': None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
if not full_url:
|
||||
logger.error("upload_to_oss 返回了空 URL")
|
||||
return Response(
|
||||
{'code': 500, 'message': '图片上传失败', 'data': None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
cos_domain = getattr(settings, 'COS_DOMAIN', '')
|
||||
if cos_domain and full_url.startswith(cos_domain):
|
||||
relative_path = full_url[len(cos_domain):].lstrip('/')
|
||||
else:
|
||||
relative_path = oss_path
|
||||
logger.info(f"相对路径: {relative_path}")
|
||||
|
||||
old_url = guanshi.invite_qrcode_url
|
||||
if old_url:
|
||||
logger.info(f"删除旧二维码: {old_url}")
|
||||
try:
|
||||
delete_from_oss(old_url)
|
||||
except Exception as e:
|
||||
logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}")
|
||||
|
||||
with transaction.atomic():
|
||||
guanshi.invite_qrcode_url = relative_path
|
||||
guanshi.save(update_fields=['invite_qrcode_url'])
|
||||
logger.info("数据库更新成功")
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'message': 'success',
|
||||
'data': {'url': relative_path}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True)
|
||||
return Response(
|
||||
{'code': 500, 'message': '服务器内部错误', 'data': None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
def get_fresh_access_token(self):
|
||||
"""
|
||||
直接请求微信接口获取新的 access_token,不缓存,保证最新。
|
||||
"""
|
||||
appid = getattr(settings, 'WEIXIN_APPID', '')
|
||||
secret = getattr(settings, 'WEIXIN_SECRET', '')
|
||||
if not appid or not secret:
|
||||
logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置")
|
||||
return None
|
||||
|
||||
url = 'https://api.weixin.qq.com/cgi-bin/token'
|
||||
params = {
|
||||
'grant_type': 'client_credential',
|
||||
'appid': appid,
|
||||
'secret': secret
|
||||
}
|
||||
try:
|
||||
logger.info("正在请求微信接口获取最新 access_token...")
|
||||
resp = requests.get(url, params=params, timeout=5)
|
||||
data = resp.json()
|
||||
new_token = data.get('access_token')
|
||||
if new_token:
|
||||
logger.info("成功获取最新 access_token")
|
||||
return new_token
|
||||
else:
|
||||
errcode = data.get('errcode', '未知')
|
||||
errmsg = data.get('errmsg', '未知错误')
|
||||
logger.error(f"获取 access_token 失败,errcode={errcode}, errmsg={errmsg}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ZuzhangHaibaoView(APIView):
|
||||
"""
|
||||
组长专属海报二维码生成接口
|
||||
路径:/peizhi/zuzhanghb
|
||||
方法:POST
|
||||
权限:JWT认证,仅限组长身份
|
||||
返回:
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"url": "erweima/zuzhang/1000001_1234567890.png",
|
||||
"yaoqingma": "ABC123XYZ"
|
||||
}
|
||||
}
|
||||
"""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
current_user = request.user
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
from utils.weixin_token import get_club_mini_access_token
|
||||
|
||||
club_id = resolve_effective_club_id(request, current_user)
|
||||
|
||||
# 1. 验证组长身份
|
||||
try:
|
||||
zuzhang = current_user.ZuzhangProfile
|
||||
except UserZuzhang.DoesNotExist:
|
||||
return Response({
|
||||
'code': 403,
|
||||
'message': '当前用户不是组长身份',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# 2. 验证组长账号状态
|
||||
if zuzhang.zhuangtai != 1:
|
||||
return Response({
|
||||
'code': 403,
|
||||
'message': '组长账号状态异常,无法生成海报',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# 3. 获取或生成邀请码
|
||||
invite_code = zuzhang.yaoqingma
|
||||
if not invite_code:
|
||||
invite_code = CreateInvitationCode(str(current_user.UserUID))
|
||||
if not VerifyInvitationCode(invite_code):
|
||||
logger.error(f"组长邀请码生成失败,用户: {current_user.UserUID}")
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '邀请码生成失败',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
zuzhang.yaoqingma = invite_code
|
||||
zuzhang.save(update_fields=['yaoqingma'])
|
||||
|
||||
# 4. 获取微信 access_token(按俱乐部对应小程序)
|
||||
access_token = get_club_mini_access_token(club_id, force_refresh=False)
|
||||
if not access_token:
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '获取微信access_token失败',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# ===== 修改点:使用 A 接口(getwxacode) =====
|
||||
encoded_invite = urllib.parse.quote(invite_code, safe='')
|
||||
# 新小程序:打手端 Tab「我的」管事注册;旧码 guanshiduan 由小程序兼容页转发
|
||||
page_path = f'pages/fighter/fighter?registerType=guanshi&inviteCode={encoded_invite}'
|
||||
|
||||
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
|
||||
if wx_error and is_weixin_token_invalid(wx_error.get('errcode'), wx_error.get('errmsg', '')):
|
||||
logger.warning('组长海报二维码 token 失效,强制刷新后重试 club=%s', club_id)
|
||||
access_token = get_club_mini_access_token(club_id, force_refresh=True)
|
||||
if not access_token:
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '获取微信access_token失败',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
|
||||
|
||||
if wx_error:
|
||||
errmsg = wx_error.get('errmsg', '未知错误')
|
||||
logger.error(f"微信接口返回错误: {errmsg}")
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'生成二维码失败: {errmsg}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
if wx_resp is None:
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '调用微信服务失败',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# 5. 上传到腾讯云 COS
|
||||
timestamp = int(time.time())
|
||||
filename = f"{current_user.UserUID}_{timestamp}.png"
|
||||
oss_path = f"erweima/zuzhang/{club_id}/{filename}"
|
||||
file_obj = io.BytesIO(wx_resp.content)
|
||||
|
||||
try:
|
||||
full_url = upload_to_oss(file_obj, oss_path)
|
||||
except Exception as e:
|
||||
logger.error(f"上传到COS失败: {e}")
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '图片上传失败',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
if not full_url:
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '图片上传失败',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# 6. 获取相对路径
|
||||
cos_domain = getattr(settings, 'COS_DOMAIN', '')
|
||||
if cos_domain and full_url.startswith(cos_domain):
|
||||
relative_path = full_url[len(cos_domain):].lstrip('/')
|
||||
else:
|
||||
relative_path = oss_path
|
||||
|
||||
# 7. 删除旧二维码文件
|
||||
old_url = zuzhang.haibao_url
|
||||
if old_url:
|
||||
delete_from_oss(old_url)
|
||||
|
||||
# 8. 更新数据库中的 haibao_url 字段
|
||||
with transaction.atomic():
|
||||
zuzhang.haibao_url = relative_path
|
||||
zuzhang.save(update_fields=['haibao_url'])
|
||||
|
||||
# 9. 返回邀请码和二维码相对路径
|
||||
return Response({
|
||||
'code': 0,
|
||||
'message': 'success',
|
||||
'data': {
|
||||
'url': relative_path,
|
||||
'yaoqingma': invite_code
|
||||
}
|
||||
})
|
||||
|
||||
def _request_wx_qrcode(self, page_path, access_token):
|
||||
"""
|
||||
调用 getwxacode 生成小程序码。
|
||||
返回 (response, error_dict);成功时 error_dict 为 None。
|
||||
"""
|
||||
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
|
||||
post_data = {
|
||||
'path': page_path,
|
||||
'width': 430,
|
||||
'auto_color': False,
|
||||
'line_color': {'r': 0, 'g': 0, 'b': 0},
|
||||
}
|
||||
try:
|
||||
wx_resp = requests.post(wx_url, json=post_data, timeout=10)
|
||||
except Exception as e:
|
||||
logger.error(f'请求微信接口失败: {e}')
|
||||
return None, {'errmsg': '调用微信服务失败'}
|
||||
|
||||
if wx_resp.status_code != 200:
|
||||
error_info = {}
|
||||
if wx_resp.headers.get('content-type', '').startswith('application/json'):
|
||||
try:
|
||||
error_info = wx_resp.json()
|
||||
except Exception:
|
||||
pass
|
||||
return wx_resp, {
|
||||
'errcode': error_info.get('errcode'),
|
||||
'errmsg': error_info.get('errmsg', f'HTTP {wx_resp.status_code}'),
|
||||
}
|
||||
|
||||
content_type = wx_resp.headers.get('content-type', '')
|
||||
if 'image' not in content_type:
|
||||
try:
|
||||
error_info = wx_resp.json()
|
||||
return wx_resp, {
|
||||
'errcode': error_info.get('errcode'),
|
||||
'errmsg': error_info.get('errmsg', '未知错误'),
|
||||
}
|
||||
except Exception:
|
||||
return wx_resp, {'errmsg': '微信接口返回未知内容'}
|
||||
|
||||
return wx_resp, None
|
||||
|
||||
|
||||
|
||||
|
||||
365
config/views/scripts.py
Normal file
365
config/views/scripts.py
Normal file
@@ -0,0 +1,365 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
import secrets
|
||||
import hashlib
|
||||
import threading
|
||||
import urllib.parse
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Q, F, Max, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.core.cache import cache
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils import timezone
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||
|
||||
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||
# from utils.ip_security import *
|
||||
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||
|
||||
from backend.utils import update_shangjia_daily
|
||||
from utils.chat_utils import subscribe_merchant_link_chat
|
||||
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||
|
||||
from ..models import (
|
||||
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||
MiniappScriptScene, MiniappScriptAutoReply,
|
||||
)
|
||||
|
||||
from users.models import (
|
||||
AdminProfile, UserDashou, UserBoss,
|
||||
UserShangjia, UserGuanshi, UserZuzhang
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
|
||||
from rank.models import Chenghao, DingdanBiaoqian
|
||||
|
||||
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||
|
||||
from ..serializers import PopupConfigSerializer
|
||||
import traceback
|
||||
import requests
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 小程序客服话术 / 自动回复默认值(原位于 miniapp_config.py,按功能组归类到话术模块)
|
||||
MINIAPP_SCRIPT_DEFAULTS = {
|
||||
'cs_welcome': {
|
||||
'title': '',
|
||||
'content': '你好,请问有什么可以帮到您的?',
|
||||
'confirm_text': '',
|
||||
'cancel_text': '',
|
||||
},
|
||||
'chat_image_confirm': {
|
||||
'title': '温馨提示',
|
||||
'content': '平台禁止任何违法违规行为,聊天内容仅限于商家与接单员正常业务对接。严禁赌博、诈骗及其他非法行为,违者后果自负。',
|
||||
'confirm_text': '确认',
|
||||
'cancel_text': '取消',
|
||||
},
|
||||
'merchant_dispatch_confirm': {
|
||||
'title': '确认派单',
|
||||
'content': '确认向该打手派发此订单?',
|
||||
'confirm_text': '确认',
|
||||
'cancel_text': '取消',
|
||||
},
|
||||
}
|
||||
|
||||
def _resolve_script_club_id(request):
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
from jituan.services.club_user import get_user_club_id
|
||||
user = getattr(request, 'user', None)
|
||||
club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None
|
||||
if not club_id:
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
return club_id or 'xq'
|
||||
|
||||
|
||||
def _merge_script_scene(scene_key, row):
|
||||
base = dict(MINIAPP_SCRIPT_DEFAULTS.get(scene_key) or {})
|
||||
if not row:
|
||||
return base
|
||||
if row.title:
|
||||
base['title'] = row.title
|
||||
if row.content:
|
||||
base['content'] = row.content
|
||||
if row.confirm_text:
|
||||
base['confirm_text'] = row.confirm_text
|
||||
if row.cancel_text:
|
||||
base['cancel_text'] = row.cancel_text
|
||||
return base
|
||||
|
||||
|
||||
class HuashuHqView(APIView):
|
||||
"""POST /peizhi/huashuhq 批量获取场景话术"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
scene_keys = request.data.get('scene_keys') or []
|
||||
if not isinstance(scene_keys, list) or not scene_keys:
|
||||
return Response({'code': 400, 'msg': 'scene_keys 不能为空', 'data': None}, status=400)
|
||||
club_id = _resolve_script_club_id(request)
|
||||
rows = MiniappScriptScene.query.filter(
|
||||
club_id=club_id, scene_key__in=scene_keys, enabled=True,
|
||||
)
|
||||
row_map = {r.scene_key: r for r in rows}
|
||||
data = {}
|
||||
for key in scene_keys:
|
||||
if not isinstance(key, str) or not key.strip():
|
||||
continue
|
||||
data[key.strip()] = _merge_script_scene(key.strip(), row_map.get(key.strip()))
|
||||
return Response({'code': 200, 'msg': 'success', 'data': data})
|
||||
|
||||
|
||||
class HuashuMatchView(APIView):
|
||||
"""POST /peizhi/huashu_match 客服关键词自动回复匹配"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
scene_key = (request.data.get('scene_key') or '').strip()
|
||||
user_text = (request.data.get('user_text') or '').strip()
|
||||
if scene_key != 'cs_auto_reply':
|
||||
return Response({'code': 400, 'msg': '不支持的 scene_key', 'data': {'matched': False, 'content': ''}})
|
||||
if not user_text:
|
||||
return Response({'code': 200, 'msg': 'success', 'data': {'matched': False, 'content': ''}})
|
||||
club_id = _resolve_script_club_id(request)
|
||||
rules = MiniappScriptAutoReply.query.filter(
|
||||
club_id=club_id, enabled=True,
|
||||
).order_by('-sort_order', 'id')
|
||||
text_lower = user_text.lower()
|
||||
for rule in rules:
|
||||
kw_raw = (rule.keyword or '').strip()
|
||||
if not kw_raw:
|
||||
continue
|
||||
keywords = [k.strip() for k in kw_raw.split(',') if k.strip()]
|
||||
for kw in keywords:
|
||||
if kw.lower() in text_lower or kw in user_text:
|
||||
reply_type = rule.reply_type or MiniappScriptAutoReply.REPLY_TEXT
|
||||
return Response({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
'data': {
|
||||
'matched': True,
|
||||
'content': rule.reply_content or '',
|
||||
'reply_type': reply_type,
|
||||
},
|
||||
})
|
||||
return Response({'code': 200, 'msg': 'success', 'data': {'matched': False, 'content': ''}})
|
||||
|
||||
|
||||
# ---------- 后台:客服话术配置(/houtai/hthqhs、/houtai/htxghs) ----------
|
||||
|
||||
SCRIPT_CONFIG_PERMS = ('kfhs666', '8080a')
|
||||
SCRIPT_SCENE_KEYS = ('chat_image_confirm', 'cs_welcome', 'merchant_dispatch_confirm')
|
||||
|
||||
|
||||
def _has_script_config_perm(permissions):
|
||||
return any(code in permissions for code in SCRIPT_CONFIG_PERMS)
|
||||
|
||||
|
||||
def _script_scene_item_type(scene_key):
|
||||
return 'text_display' if scene_key == 'cs_welcome' else 'confirm_modal'
|
||||
|
||||
|
||||
def _serialize_script_scene(row):
|
||||
return {
|
||||
'id': row.id,
|
||||
'scene_key': row.scene_key,
|
||||
'item_type': _script_scene_item_type(row.scene_key),
|
||||
'title': row.title or '',
|
||||
'content': row.content or '',
|
||||
'confirm_text': row.confirm_text or '',
|
||||
'cancel_text': row.cancel_text or '',
|
||||
'is_active': bool(row.enabled),
|
||||
}
|
||||
|
||||
|
||||
def _serialize_script_auto_reply(row):
|
||||
keywords = [k.strip() for k in (row.keyword or '').split(',') if k.strip()]
|
||||
return {
|
||||
'id': row.id,
|
||||
'scene_key': 'cs_auto_reply',
|
||||
'item_type': 'auto_reply',
|
||||
'title': '',
|
||||
'keywords': keywords,
|
||||
'match_mode': 'contains',
|
||||
'priority': row.sort_order or 0,
|
||||
'content': row.reply_content or '',
|
||||
'is_active': bool(row.enabled),
|
||||
}
|
||||
|
||||
|
||||
class MiniappScriptHoutaiListView(APIView):
|
||||
"""POST /houtai/hthqhs 后台获取话术与自动回复配置"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
from backend.utils import verify_kefu_permission
|
||||
|
||||
username = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||
if not _has_script_config_perm(permissions):
|
||||
return Response({'code': 403, 'msg': '您没有权限访问客服聊天配置'})
|
||||
|
||||
club_id = _resolve_script_club_id(request)
|
||||
scenes = MiniappScriptScene.query.filter(
|
||||
club_id=club_id, scene_key__in=SCRIPT_SCENE_KEYS,
|
||||
).order_by('scene_key')
|
||||
auto_rules = MiniappScriptAutoReply.query.filter(club_id=club_id).order_by('-sort_order', 'id')
|
||||
|
||||
items = [_serialize_script_scene(r) for r in scenes]
|
||||
items.extend(_serialize_script_auto_reply(r) for r in auto_rules)
|
||||
return Response({'code': 0, 'data': {'items': items, 'club_id': club_id}})
|
||||
|
||||
|
||||
class MiniappScriptHoutaiModifyView(APIView):
|
||||
"""POST /houtai/htxghs 后台增删改话术与自动回复"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
from backend.utils import verify_kefu_permission
|
||||
from jituan.services.display_config import forbid_display_write_in_all_scope
|
||||
|
||||
username = request.data.get('username')
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||
if not _has_script_config_perm(permissions):
|
||||
return Response({'code': 403, 'msg': '您没有权限操作客服聊天配置'})
|
||||
|
||||
action = (request.data.get('action') or '').strip()
|
||||
if not action:
|
||||
return Response({'code': 400, 'msg': '缺少 action 参数'})
|
||||
|
||||
club_id = _resolve_script_club_id(request)
|
||||
|
||||
if action == 'delete':
|
||||
return self._delete_item(request, club_id)
|
||||
|
||||
deny = forbid_display_write_in_all_scope(request)
|
||||
if deny:
|
||||
return deny
|
||||
|
||||
if action in ('create', 'update'):
|
||||
item_type = (request.data.get('item_type') or '').strip()
|
||||
if item_type == 'auto_reply':
|
||||
return self._save_auto_reply(request, club_id, action)
|
||||
return self._save_scene(request, club_id, action)
|
||||
|
||||
return Response({'code': 400, 'msg': f'未知的 action: {action}'})
|
||||
|
||||
def _save_scene(self, request, club_id, action):
|
||||
scene_key = (request.data.get('scene_key') or '').strip()
|
||||
if scene_key not in SCRIPT_SCENE_KEYS:
|
||||
return Response({'code': 400, 'msg': '不支持的 scene_key'})
|
||||
|
||||
content = (request.data.get('content') or '').strip()
|
||||
if not content:
|
||||
return Response({'code': 400, 'msg': '请填写正文内容'})
|
||||
|
||||
fields = {
|
||||
'title': (request.data.get('title') or '').strip(),
|
||||
'content': content,
|
||||
'confirm_text': (request.data.get('confirm_text') or '').strip(),
|
||||
'cancel_text': (request.data.get('cancel_text') or '').strip(),
|
||||
'enabled': bool(request.data.get('is_active', True)),
|
||||
}
|
||||
|
||||
if action == 'update':
|
||||
row_id = request.data.get('id')
|
||||
if not row_id:
|
||||
return Response({'code': 400, 'msg': '缺少 id'})
|
||||
row = MiniappScriptScene.query.filter(id=row_id, club_id=club_id).first()
|
||||
if not row:
|
||||
return Response({'code': 404, 'msg': '配置不存在'})
|
||||
for k, v in fields.items():
|
||||
setattr(row, k, v)
|
||||
row.save()
|
||||
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_scene(row)})
|
||||
|
||||
existing = MiniappScriptScene.query.filter(club_id=club_id, scene_key=scene_key).first()
|
||||
if existing:
|
||||
for k, v in fields.items():
|
||||
setattr(existing, k, v)
|
||||
existing.save()
|
||||
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_scene(existing)})
|
||||
|
||||
row = MiniappScriptScene.query.create(club_id=club_id, scene_key=scene_key, **fields)
|
||||
return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_script_scene(row)})
|
||||
|
||||
def _save_auto_reply(self, request, club_id, action):
|
||||
keywords = request.data.get('keywords') or []
|
||||
if not isinstance(keywords, list) or not keywords:
|
||||
return Response({'code': 400, 'msg': '请至少添加一个触发关键词'})
|
||||
|
||||
kw_list = [str(k).strip() for k in keywords if str(k).strip()]
|
||||
if not kw_list:
|
||||
return Response({'code': 400, 'msg': '请至少添加一个触发关键词'})
|
||||
|
||||
content = (request.data.get('content') or '').strip()
|
||||
if not content:
|
||||
return Response({'code': 400, 'msg': '请填写回复内容'})
|
||||
|
||||
fields = {
|
||||
'keyword': ','.join(kw_list),
|
||||
'reply_content': content,
|
||||
'sort_order': int(request.data.get('priority') or 0),
|
||||
'enabled': bool(request.data.get('is_active', True)),
|
||||
'reply_type': MiniappScriptAutoReply.REPLY_TEXT,
|
||||
}
|
||||
|
||||
if action == 'update':
|
||||
row_id = request.data.get('id')
|
||||
if not row_id:
|
||||
return Response({'code': 400, 'msg': '缺少 id'})
|
||||
row = MiniappScriptAutoReply.query.filter(id=row_id, club_id=club_id).first()
|
||||
if not row:
|
||||
return Response({'code': 404, 'msg': '规则不存在'})
|
||||
for k, v in fields.items():
|
||||
setattr(row, k, v)
|
||||
row.save()
|
||||
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_auto_reply(row)})
|
||||
|
||||
row = MiniappScriptAutoReply.query.create(club_id=club_id, **fields)
|
||||
return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_script_auto_reply(row)})
|
||||
|
||||
def _delete_item(self, request, club_id):
|
||||
from jituan.services.display_config import forbid_display_write_in_all_scope
|
||||
|
||||
deny = forbid_display_write_in_all_scope(request)
|
||||
if deny:
|
||||
return deny
|
||||
|
||||
row_id = request.data.get('id')
|
||||
if not row_id:
|
||||
return Response({'code': 400, 'msg': '缺少 id'})
|
||||
|
||||
row = MiniappScriptAutoReply.query.filter(id=row_id, club_id=club_id).first()
|
||||
if row:
|
||||
row.delete()
|
||||
return Response({'code': 0, 'msg': '删除成功'})
|
||||
|
||||
row = MiniappScriptScene.query.filter(id=row_id, club_id=club_id).first()
|
||||
if row:
|
||||
return Response({'code': 400, 'msg': '场景话术不支持删除,请改为禁用'})
|
||||
|
||||
return Response({'code': 404, 'msg': '记录不存在'})
|
||||
1
data/ai_chat/context.json
Normal file
1
data/ai_chat/context.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
73
deploy/pre_migrate_dedupe.sql
Normal file
73
deploy/pre_migrate_dedupe.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
-- 部署 jituan 前若 migrate 报 Duplicate entry 唯一约束冲突时使用。
|
||||
-- 不要用 mysql 命令行(1Panel MySQL 不在本机 socket),请用:
|
||||
-- python manage.py dbshell < deploy/pre_migrate_dedupe.sql
|
||||
-- 或半路失败的 config.0006:
|
||||
-- python manage.py recover_config_0006
|
||||
-- python manage.py migrate config 0006 --fake
|
||||
-- python manage.py migrate
|
||||
|
||||
-- config.0006 tupianpeizhi
|
||||
DELETE FROM tupianpeizhi
|
||||
WHERE id NOT IN (
|
||||
SELECT keep_id FROM (
|
||||
SELECT MAX(id) AS keep_id FROM tupianpeizhi GROUP BY club_id, ImageType
|
||||
) AS t
|
||||
);
|
||||
|
||||
-- config.0006 popup_page
|
||||
DELETE FROM popup_page
|
||||
WHERE id NOT IN (
|
||||
SELECT keep_id FROM (
|
||||
SELECT MAX(id) AS keep_id FROM popup_page GROUP BY club_id, page_key
|
||||
) AS t
|
||||
);
|
||||
|
||||
-- products.0005 duoci_fenhong
|
||||
DELETE FROM duoci_fenhong
|
||||
WHERE id NOT IN (
|
||||
SELECT keep_id FROM (
|
||||
SELECT MAX(id) AS keep_id FROM duoci_fenhong GROUP BY club_id, huiyuan, yonghuid, cishu
|
||||
) AS t
|
||||
);
|
||||
|
||||
-- backend.0003 提现日统计
|
||||
DELETE FROM tixian_ri_tongji
|
||||
WHERE id NOT IN (
|
||||
SELECT keep_id FROM (
|
||||
SELECT MAX(id) AS keep_id FROM tixian_ri_tongji GROUP BY club_id, Date, WithdrawalType
|
||||
) AS t
|
||||
);
|
||||
|
||||
-- backend.0004 角色日统计
|
||||
DELETE FROM shangjia_ri_tongji
|
||||
WHERE id NOT IN (
|
||||
SELECT keep_id FROM (
|
||||
SELECT MAX(id) AS keep_id FROM shangjia_ri_tongji GROUP BY club_id, MerchantID, Date
|
||||
) AS t
|
||||
);
|
||||
DELETE FROM dashou_ri_tongji
|
||||
WHERE id NOT IN (
|
||||
SELECT keep_id FROM (
|
||||
SELECT MAX(id) AS keep_id FROM dashou_ri_tongji GROUP BY club_id, PlayerID, Date
|
||||
) AS t
|
||||
);
|
||||
DELETE FROM zuzhang_ri_tongji
|
||||
WHERE id NOT IN (
|
||||
SELECT keep_id FROM (
|
||||
SELECT MAX(id) AS keep_id FROM zuzhang_ri_tongji GROUP BY club_id, LeaderID, Date
|
||||
) AS t
|
||||
);
|
||||
DELETE FROM guanshi_ri_tongji
|
||||
WHERE id NOT IN (
|
||||
SELECT keep_id FROM (
|
||||
SELECT MAX(id) AS keep_id FROM guanshi_ri_tongji GROUP BY club_id, ManagerID, Date
|
||||
) AS t
|
||||
);
|
||||
DELETE FROM guanshi_xufei_ri_tongji
|
||||
WHERE id NOT IN (
|
||||
SELECT keep_id FROM (
|
||||
SELECT MAX(id) AS keep_id FROM guanshi_xufei_ri_tongji GROUP BY club_id, ManagerID, Date
|
||||
) AS t
|
||||
);
|
||||
|
||||
-- szjilu 应只有 1 行;多行时勿自动删,需人工核对 TotalIncome 后处理
|
||||
111
deploy/seed_shop_permissions.sql
Normal file
111
deploy/seed_shop_permissions.sql
Normal file
@@ -0,0 +1,111 @@
|
||||
-- ============================================================
|
||||
-- 店铺相关权限:查询 + 缺失则插入
|
||||
-- 主权限表:Permission(gvsdsdk RBAC,角色管理页读取此表)
|
||||
-- 兼容旧表:permission(legacy,部分账号仍走 user_role.account_id=手机号)
|
||||
-- ============================================================
|
||||
|
||||
-- ---------- 1. 查询 gvsdsdk 主权限表 ----------
|
||||
SELECT PermCode, PermName, PermDesc, PermCategory, PermStatus
|
||||
FROM `Permission`
|
||||
WHERE PermCode IN (
|
||||
'999a', '999b', '999c', '999d', '999e',
|
||||
'1199ab', '1199abc', '1199abd', '1199abe'
|
||||
)
|
||||
ORDER BY PermCode;
|
||||
|
||||
-- ---------- 2. 查询 legacy 权限表(如仍在使用) ----------
|
||||
SELECT perm_id, perm_code, perm_name, description
|
||||
FROM `permission`
|
||||
WHERE perm_code IN (
|
||||
'999a', '999b', '999c', '999d', '999e',
|
||||
'1199ab', '1199abc', '1199abd', '1199abe'
|
||||
)
|
||||
ORDER BY perm_code;
|
||||
|
||||
-- ---------- 3. 缺失则写入 Permission(主表,推荐) ----------
|
||||
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
|
||||
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺状态管理', '999a', '店铺管理', 1, '修改店铺封禁/正常状态', NOW()
|
||||
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '999a');
|
||||
|
||||
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
|
||||
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺余额与创建', '999b', '店铺管理', 1, '修改可提现余额、添加新店铺', NOW()
|
||||
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '999b');
|
||||
|
||||
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
|
||||
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺提现限额', '999c', '店铺管理', 1, '修改每日提现限额及开关', NOW()
|
||||
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '999c');
|
||||
|
||||
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
|
||||
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺抽成配置', '999d', '店铺管理', 1, '修改店铺订单收益抽成及默认抽成费率', NOW()
|
||||
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '999d');
|
||||
|
||||
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
|
||||
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺基本信息', '999e', '店铺管理', 1, '修改店铺名称、电话、微信号', NOW()
|
||||
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '999e');
|
||||
|
||||
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
|
||||
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺商品审核模式', '1199ab', '店铺管理', 1, '查看/修改店铺商品全局审核开关', NOW()
|
||||
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '1199ab');
|
||||
|
||||
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
|
||||
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺商品封禁下架', '1199abc', '店铺管理', 1, '封禁、下架、驳回店铺商品/类型审核', NOW()
|
||||
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '1199abc');
|
||||
|
||||
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
|
||||
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺商品上架解封', '1199abd', '店铺管理', 1, '上架、解封、审核通过店铺商品/类型', NOW()
|
||||
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '1199abd');
|
||||
|
||||
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
|
||||
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺商品信息修改', '1199abe', '店铺管理', 1, '修改店铺商品标题、价格、库存等基本信息', NOW()
|
||||
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '1199abe');
|
||||
|
||||
-- ---------- 4. 可选:legacy permission 表(若你们仍在用旧角色表) ----------
|
||||
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
|
||||
SELECT '999a', '店铺状态管理', '修改店铺封禁/正常状态', NOW(), NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '999a');
|
||||
|
||||
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
|
||||
SELECT '999b', '店铺余额与创建', '修改可提现余额、添加新店铺', NOW(), NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '999b');
|
||||
|
||||
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
|
||||
SELECT '999c', '店铺提现限额', '修改每日提现限额及开关', NOW(), NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '999c');
|
||||
|
||||
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
|
||||
SELECT '999d', '店铺抽成配置', '修改店铺订单收益抽成及默认抽成费率', NOW(), NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '999d');
|
||||
|
||||
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
|
||||
SELECT '999e', '店铺基本信息', '修改店铺名称、电话、微信号', NOW(), NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '999e');
|
||||
|
||||
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
|
||||
SELECT '1199ab', '店铺商品审核模式', '查看/修改店铺商品全局审核开关', NOW(), NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '1199ab');
|
||||
|
||||
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
|
||||
SELECT '1199abc', '店铺商品封禁下架', '封禁、下架、驳回店铺商品/类型审核', NOW(), NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '1199abc');
|
||||
|
||||
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
|
||||
SELECT '1199abd', '店铺商品上架解封', '上架、解封、审核通过店铺商品/类型', NOW(), NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '1199abd');
|
||||
|
||||
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
|
||||
SELECT '1199abe', '店铺商品信息修改', '修改店铺商品标题、价格、库存等基本信息', NOW(), NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '1199abe');
|
||||
|
||||
-- ---------- 5. 插入后再查一遍确认 ----------
|
||||
SELECT PermCode, PermName, PermStatus FROM `Permission`
|
||||
WHERE PermCode IN ('999a','999b','999c','999d','999e','1199ab','1199abc','1199abd','1199abe')
|
||||
ORDER BY PermCode;
|
||||
163
find.py
163
find.py
@@ -1,163 +0,0 @@
|
||||
"""
|
||||
REPL 模式文件内容搜索工具
|
||||
支持正则或纯文本搜索,默认搜索当前目录
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# 硬编码排除目录
|
||||
EXCLUDE_DIRS = {
|
||||
'.git', '.vs', 'node_modules', '__pycache__', '.idea',
|
||||
'miniprogram_npm', '_backup_pre_redesign',
|
||||
}
|
||||
|
||||
# 排除的文件扩展名
|
||||
EXCLUDE_EXTS = {
|
||||
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.bmp',
|
||||
'.mp3', '.mp4', '.wav', '.avi',
|
||||
'.zip', '.rar', '.7z', '.tar', '.gz',
|
||||
'.exe', '.dll', '.so', '.dylib',
|
||||
'.pyc', '.pyo', '.class', '.o', '.obj',
|
||||
'.db', '.sqlite', '.vsidx',
|
||||
}
|
||||
|
||||
# 默认搜索根目录
|
||||
DEFAULT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def should_skip(dirpath):
|
||||
"""判断目录是否应跳过"""
|
||||
parts = dirpath.replace('\\', '/').split('/')
|
||||
return any(p in EXCLUDE_DIRS for p in parts)
|
||||
|
||||
|
||||
def search(pattern, root=DEFAULT_ROOT, use_regex=True, max_results=50):
|
||||
"""搜索文件内容"""
|
||||
try:
|
||||
if use_regex:
|
||||
compiled = re.compile(pattern, re.IGNORECASE)
|
||||
except re.error as e:
|
||||
print(f' 正则语法错误: {e}')
|
||||
return
|
||||
|
||||
count = 0
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
# 就地修改 dirnames 以跳过排除目录
|
||||
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
|
||||
|
||||
for fname in filenames:
|
||||
ext = os.path.splitext(fname)[1].lower()
|
||||
if ext in EXCLUDE_EXTS:
|
||||
continue
|
||||
|
||||
fpath = os.path.join(dirpath, fname)
|
||||
relpath = os.path.relpath(fpath, root)
|
||||
try:
|
||||
with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for lineno, line in enumerate(f, 1):
|
||||
line_stripped = line.rstrip('\n\r')
|
||||
if use_regex:
|
||||
match = compiled.search(line_stripped)
|
||||
if match:
|
||||
highlight = line_stripped[:match.start()] + \
|
||||
f'>>> {match.group()} <<<' + \
|
||||
line_stripped[match.end():]
|
||||
print(f' {relpath}:{lineno} {highlight}')
|
||||
count += 1
|
||||
else:
|
||||
if pattern.lower() in line_stripped.lower():
|
||||
print(f' {relpath}:{lineno} {line_stripped}')
|
||||
count += 1
|
||||
|
||||
if count >= max_results:
|
||||
print(f'\n ... 已达上限 {max_results} 条,停止')
|
||||
return
|
||||
except (PermissionError, OSError):
|
||||
continue
|
||||
|
||||
if count == 0:
|
||||
print(' 未找到匹配内容')
|
||||
else:
|
||||
print(f'\n 共 {count} 条匹配')
|
||||
|
||||
|
||||
def main():
|
||||
print('=== 文件内容搜索工具 ===')
|
||||
print(f'搜索根目录: {DEFAULT_ROOT}')
|
||||
print(f'排除目录: {EXCLUDE_DIRS}')
|
||||
print()
|
||||
print('命令:')
|
||||
print(' /regex <pattern> 正则搜索')
|
||||
print(' /text <text> 纯文本搜索')
|
||||
print(' /max <n> 设置结果上限(默认50)')
|
||||
print(' /dir <path> 切换搜索目录')
|
||||
print(' /help 显示帮助')
|
||||
print(' /quit 退出')
|
||||
print()
|
||||
print('直接输入内容默认使用正则搜索')
|
||||
print()
|
||||
|
||||
root = DEFAULT_ROOT
|
||||
use_regex = True
|
||||
max_results = 50
|
||||
|
||||
while True:
|
||||
try:
|
||||
raw = input('find> ').strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print('\n再见')
|
||||
break
|
||||
|
||||
if not raw:
|
||||
continue
|
||||
|
||||
if raw == '/quit':
|
||||
print('再见')
|
||||
break
|
||||
|
||||
if raw == '/help':
|
||||
print(' /regex <pattern> 正则搜索')
|
||||
print(' /text <text> 纯文本搜索')
|
||||
print(' /max <n> 设置结果上限')
|
||||
print(' /dir <path> 切换搜索目录')
|
||||
print(' /help 显示帮助')
|
||||
print(' /quit 退出')
|
||||
print(' 直接输入内容默认使用正则搜索')
|
||||
continue
|
||||
|
||||
if raw.startswith('/regex '):
|
||||
pattern = raw[7:].strip()
|
||||
if pattern:
|
||||
search(pattern, root, use_regex=True, max_results=max_results)
|
||||
continue
|
||||
|
||||
if raw.startswith('/text '):
|
||||
pattern = raw[6:].strip()
|
||||
if pattern:
|
||||
search(pattern, root, use_regex=False, max_results=max_results)
|
||||
continue
|
||||
|
||||
if raw.startswith('/max '):
|
||||
try:
|
||||
max_results = int(raw[5:].strip())
|
||||
print(f' 结果上限设为 {max_results}')
|
||||
except ValueError:
|
||||
print(' 请输入数字')
|
||||
continue
|
||||
|
||||
if raw.startswith('/dir '):
|
||||
new_dir = raw[5:].strip()
|
||||
if os.path.isdir(new_dir):
|
||||
root = os.path.abspath(new_dir)
|
||||
print(f' 搜索目录切换为: {root}')
|
||||
else:
|
||||
print(f' 目录不存在: {new_dir}')
|
||||
continue
|
||||
|
||||
# 默认正则搜索
|
||||
search(raw, root, use_regex=use_regex, max_results=max_results)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,35 +0,0 @@
|
||||
"""
|
||||
修复 django_migrations 表中的旧 app 名称引用。
|
||||
在服务器上执行: python manage.py shell < fix_migration_history.py
|
||||
或: python manage.py shell
|
||||
然后粘贴此脚本内容
|
||||
"""
|
||||
from django.db import connection
|
||||
|
||||
APP_NAME_MAPPINGS = {
|
||||
'houtai': 'backend',
|
||||
'yonghu': 'users',
|
||||
'dingdan': 'orders',
|
||||
'peizhi': 'config',
|
||||
'shangpin': 'products',
|
||||
'shangdian': 'shop',
|
||||
'dengji': 'rank',
|
||||
}
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
for old_name, new_name in APP_NAME_MAPPINGS.items():
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM django_migrations WHERE app = %s",
|
||||
[old_name]
|
||||
)
|
||||
count = cursor.fetchone()[0]
|
||||
if count > 0:
|
||||
cursor.execute(
|
||||
"UPDATE django_migrations SET app = %s WHERE app = %s",
|
||||
[new_name, old_name]
|
||||
)
|
||||
print(f"已更新 {count} 条记录: {old_name} -> {new_name}")
|
||||
else:
|
||||
print(f"无需更新: {old_name} (无记录)")
|
||||
|
||||
print("\n修复完成!请运行 python manage.py migrate")
|
||||
@@ -13,7 +13,8 @@ class ELTAuthentication:
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
from gvsdsdk.models import User, EphemeralToken
|
||||
from gvsdsdk.models import EphemeralToken
|
||||
from gvsds.apps.core.models import User
|
||||
import datetime
|
||||
|
||||
try:
|
||||
|
||||
54
gvsdsdk/auth/jwt_auth.py
Normal file
54
gvsdsdk/auth/jwt_auth.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""自定义 JWT 认证 — 向后兼容旧 token 的 user_id claim"""
|
||||
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken, AuthenticationFailed
|
||||
from rest_framework_simplejwt.settings import api_settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class CompatibleJWTAuthentication(JWTAuthentication):
|
||||
"""
|
||||
兼容旧 token 的 JWT 认证类。
|
||||
|
||||
旧 token(USER_ID_CLAIM='user_id' 时生成)payload 中使用 'user_id' claim,
|
||||
值为 user.id(UUID 字符串)。
|
||||
新 token(USER_ID_CLAIM='UserUID' 时生成)payload 中使用 'UserUID' claim,
|
||||
值为 user.UserUID(7位业务ID)。
|
||||
|
||||
此类先尝试新 claim,找不到时回退到旧 claim,并用对应字段查库。
|
||||
"""
|
||||
|
||||
def get_user(self, validated_token):
|
||||
# 1. 先尝试当前配置的 USER_ID_CLAIM
|
||||
user_id = validated_token.get(api_settings.USER_ID_CLAIM)
|
||||
lookup_field = api_settings.USER_ID_FIELD
|
||||
|
||||
if user_id is None:
|
||||
# 2. 回退:尝试旧 token 中的 'user_id' claim
|
||||
user_id = validated_token.get('user_id')
|
||||
if user_id is None:
|
||||
raise InvalidToken(
|
||||
{'detail': _('令牌未包含用户标识符'), 'code': 'token_not_valid'}
|
||||
)
|
||||
# 旧 token 的 user_id 来自 user.id(即 UserUUID),
|
||||
# 需要用 UserUUID 字段查库
|
||||
lookup_field = 'UserUUID'
|
||||
|
||||
try:
|
||||
user = self.user_model.objects.get(**{lookup_field: user_id})
|
||||
except self.user_model.DoesNotExist:
|
||||
raise AuthenticationFailed(_('用户不存在'), code='user_not_found')
|
||||
|
||||
if not user.is_active:
|
||||
raise AuthenticationFailed(_('用户已被禁用'), code='user_inactive')
|
||||
|
||||
if api_settings.CHECK_REVOKE_TOKEN:
|
||||
from rest_framework_simplejwt.utils import get_md5_hash_password
|
||||
if validated_token.get(
|
||||
api_settings.REVOKE_TOKEN_CLAIM
|
||||
) != get_md5_hash_password(user.password):
|
||||
raise AuthenticationFailed(
|
||||
_('用户密码已更改'), code='password_changed'
|
||||
)
|
||||
|
||||
return user
|
||||
@@ -87,32 +87,39 @@ class RoleManager:
|
||||
@staticmethod
|
||||
def GetUsersForRole(RoleUUID):
|
||||
"""获取拥有指定角色的所有用户"""
|
||||
from gvsdsdk.models import User
|
||||
from gvsds.apps.core.models import User
|
||||
user_uuids = UserRole.objects.filter(RoleUUID=RoleUUID).values_list('UserUUID', flat=True)
|
||||
return User.objects.filter(UserUUID__in=user_uuids)
|
||||
|
||||
@staticmethod
|
||||
def AssignPermission(RoleUUID, PermUUID):
|
||||
"""为角色分配权限"""
|
||||
def AssignPermission(RoleUUID, PermUUID, CompanyUUID):
|
||||
"""为角色分配权限(指定公司)"""
|
||||
rp = RolePermission(
|
||||
RolePermUUID=uuid.uuid4().bytes,
|
||||
RoleUUID=RoleUUID,
|
||||
PermUUID=PermUUID,
|
||||
CompanyUUID=CompanyUUID,
|
||||
CreateTime=datetime.datetime.utcnow(),
|
||||
)
|
||||
rp.save()
|
||||
return rp
|
||||
|
||||
@staticmethod
|
||||
def RemovePermission(RoleUUID, PermUUID):
|
||||
def RemovePermission(RoleUUID, PermUUID, CompanyUUID=None):
|
||||
"""移除角色的指定权限"""
|
||||
deleted, _ = RolePermission.objects.filter(RoleUUID=RoleUUID, PermUUID=PermUUID).delete()
|
||||
qs = RolePermission.objects.filter(RoleUUID=RoleUUID, PermUUID=PermUUID)
|
||||
if CompanyUUID is not None:
|
||||
qs = qs.filter(CompanyUUID=CompanyUUID)
|
||||
deleted, _ = qs.delete()
|
||||
return deleted > 0
|
||||
|
||||
@staticmethod
|
||||
def GetPermissionsForRole(RoleUUID):
|
||||
"""获取角色的所有权限"""
|
||||
perm_uuids = RolePermission.objects.filter(RoleUUID=RoleUUID).values_list('PermUUID', flat=True)
|
||||
def GetPermissionsForRole(RoleUUID, CompanyUUID=None):
|
||||
"""获取角色的所有权限(可按公司过滤)"""
|
||||
qs = RolePermission.objects.filter(RoleUUID=RoleUUID)
|
||||
if CompanyUUID is not None:
|
||||
qs = qs.filter(CompanyUUID=CompanyUUID)
|
||||
perm_uuids = qs.values_list('PermUUID', flat=True)
|
||||
return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from gvsdsdk.models import User, UserRole, Role, RolePermission, Permission, UserABAC
|
||||
from gvsds.apps.core.models import User
|
||||
from gvsdsdk.models import UserRole, Role, RolePermission, Permission, UserABAC
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
|
||||
@@ -48,7 +49,7 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
||||
"""权限验证器——混合 ABAC + RBAC 检查
|
||||
|
||||
RBAC 仅检查当前角色(CurrentRoleUUID Cookie 指定的角色)的权限,
|
||||
通过 RolePermission → Permission 链路查询。
|
||||
通过 RolePermission → Permission 链路查询,按当前公司过滤。
|
||||
超级管理员/员工直接放行。
|
||||
"""
|
||||
if not permission:
|
||||
@@ -63,6 +64,7 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
||||
return True
|
||||
|
||||
current_role = getattr(request, 'current_role', None) if request else None
|
||||
current_company = getattr(request, 'current_company_uuid', None) if request else None
|
||||
|
||||
for attr in attributes:
|
||||
# ABAC 检查
|
||||
@@ -73,13 +75,16 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
||||
if abac_match:
|
||||
continue
|
||||
|
||||
# RBAC 检查:仅检查当前角色
|
||||
# RBAC 检查:仅检查当前角色,按公司过滤权限
|
||||
if current_role and isinstance(current_role, Role):
|
||||
if current_role.RoleStatus != 1:
|
||||
return False
|
||||
perm_uuids = RolePermission.objects.filter(
|
||||
rp_qs = RolePermission.objects.filter(
|
||||
RoleUUID=current_role.RoleUUID
|
||||
).values_list('PermUUID', flat=True)
|
||||
)
|
||||
if current_company:
|
||||
rp_qs = rp_qs.filter(CompanyUUID=current_company)
|
||||
perm_uuids = rp_qs.values_list('PermUUID', flat=True)
|
||||
rbac_result = Permission.objects.filter(
|
||||
PermUUID__in=perm_uuids,
|
||||
PermCode=attr,
|
||||
@@ -94,9 +99,12 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
||||
RoleUUID__in=user_role_uuids,
|
||||
RoleStatus=1,
|
||||
).values_list('RoleUUID', flat=True)
|
||||
perm_uuids = RolePermission.objects.filter(
|
||||
rp_qs = RolePermission.objects.filter(
|
||||
RoleUUID__in=active_role_uuids
|
||||
).values_list('PermUUID', flat=True)
|
||||
)
|
||||
if current_company:
|
||||
rp_qs = rp_qs.filter(CompanyUUID=current_company)
|
||||
perm_uuids = rp_qs.values_list('PermUUID', flat=True)
|
||||
rbac_result = Permission.objects.filter(
|
||||
PermUUID__in=perm_uuids,
|
||||
PermCode=attr,
|
||||
|
||||
@@ -67,7 +67,7 @@ class CommissionEvent(QModel):
|
||||
indexes = [
|
||||
models.Index(fields=['TenantUUID', 'EventType', 'EventStatus'], name='idx_cmevt_tenant_type_status'),
|
||||
models.Index(fields=['EventKey'], name='idx_cmevt_event_key'),
|
||||
models.Index(fields=['CreateTime'], name='idx_cmevt_create_time'),
|
||||
models.Index(fields=['CreateTime'], name='idx_cmevt_CreateTime'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
@@ -270,7 +270,7 @@ class CommissionRecord(QModel):
|
||||
indexes = [
|
||||
models.Index(fields=['TenantUUID', 'ParticipantID', 'Status'], name='idx_cmrec_tenant_part_status'),
|
||||
models.Index(fields=['Event'], name='idx_cmrec_event'),
|
||||
models.Index(fields=['CreateTime'], name='idx_cmrec_create_time'),
|
||||
models.Index(fields=['CreateTime'], name='idx_cmrec_CreateTime'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -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('-CreateTime').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.CreateTime)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import functools
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union, overload
|
||||
from typing import Any, Dict, Generic, Iterator, List, Optional, Sequence, Tuple, Type, TypeVar, Union
|
||||
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_local = threading.local()
|
||||
|
||||
#: 模型类型变量,用于 FluentQuery[_M] 泛型参数
|
||||
_M = TypeVar('_M', bound=models.Model)
|
||||
|
||||
|
||||
def _get_field_expression_cls():
|
||||
"""延迟导入,避免 fluent ↔ model_base 循环依赖"""
|
||||
"""延迟导入 FieldExpression,避免 fluent ↔ model_base 循环依赖。"""
|
||||
from gvsdsdk.model_base import FieldExpression
|
||||
return FieldExpression
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 兼容层聚合函数
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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.aggregate = aggregate
|
||||
|
||||
|
||||
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.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})
|
||||
|
||||
def __and__(self, other):
|
||||
def __and__(self, other) -> Q:
|
||||
if isinstance(other, Q):
|
||||
return Q(**{f'{self.field_name}__{self.lookup_type}': None}) & other
|
||||
return self.__eq__(other)
|
||||
|
||||
|
||||
class _CompatFunc:
|
||||
def sum(self, field_expr):
|
||||
if hasattr(field_expr, '_django_name'):
|
||||
agg = Sum(field_expr._django_name)
|
||||
elif hasattr(field_expr, 'field_name'):
|
||||
agg = Sum(field_expr.field_name)
|
||||
else:
|
||||
agg = Sum(str(field_expr))
|
||||
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
|
||||
"""兼容层聚合函数集合,提供 SQLAlchemy 风格的 ``func.sum()`` / ``func.count()`` 等。
|
||||
|
||||
用法::
|
||||
|
||||
from gvsdsdk.fluent import func
|
||||
|
||||
func.sum(Model.jine) → Sum('jine')
|
||||
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
|
||||
|
||||
def count(self, field_expr=None):
|
||||
def count(self, field_expr=None) -> Union[_AggregateExpr, Count]:
|
||||
"""计数聚合,无参数时等价于 COUNT(*)。"""
|
||||
if field_expr is None:
|
||||
return Count('*')
|
||||
if hasattr(field_expr, '_django_name'):
|
||||
agg = Count(field_expr._django_name)
|
||||
else:
|
||||
agg = Count(str(field_expr))
|
||||
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
|
||||
name = self._resolve_name(field_expr)
|
||||
agg = Count(name)
|
||||
model = getattr(field_expr, 'model_class', None)
|
||||
return _AggregateExpr(model, agg) if model else agg
|
||||
|
||||
def max(self, field_expr):
|
||||
if hasattr(field_expr, '_django_name'):
|
||||
agg = Max(field_expr._django_name)
|
||||
else:
|
||||
agg = Max(str(field_expr))
|
||||
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
|
||||
def max(self, field_expr) -> Union[_AggregateExpr, Max]:
|
||||
"""最大值聚合。"""
|
||||
name = self._resolve_name(field_expr)
|
||||
agg = Max(name)
|
||||
model = getattr(field_expr, 'model_class', None)
|
||||
return _AggregateExpr(model, agg) if model else agg
|
||||
|
||||
def min(self, field_expr):
|
||||
if hasattr(field_expr, '_django_name'):
|
||||
agg = Min(field_expr._django_name)
|
||||
else:
|
||||
agg = Min(str(field_expr))
|
||||
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
|
||||
def min(self, field_expr) -> Union[_AggregateExpr, Min]:
|
||||
"""最小值聚合。"""
|
||||
name = self._resolve_name(field_expr)
|
||||
agg = Min(name)
|
||||
model = getattr(field_expr, 'model_class', None)
|
||||
return _AggregateExpr(model, agg) if model else agg
|
||||
|
||||
def avg(self, field_expr):
|
||||
if hasattr(field_expr, '_django_name'):
|
||||
agg = Avg(field_expr._django_name)
|
||||
else:
|
||||
agg = Avg(str(field_expr))
|
||||
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
|
||||
def avg(self, field_expr) -> Union[_AggregateExpr, Avg]:
|
||||
"""平均值聚合。"""
|
||||
name = self._resolve_name(field_expr)
|
||||
agg = Avg(name)
|
||||
model = getattr(field_expr, 'model_class', None)
|
||||
return _AggregateExpr(model, agg) if model else agg
|
||||
|
||||
def year(self, field_expr):
|
||||
if hasattr(field_expr, '_django_name'):
|
||||
return _ExtractExpr(field_expr._django_name, 'year')
|
||||
return _ExtractExpr(str(field_expr), 'year')
|
||||
def year(self, field_expr) -> _ExtractExpr:
|
||||
"""提取年份,用于 ``func.year(Model.date) == 2024``。"""
|
||||
return _ExtractExpr(self._resolve_name(field_expr), 'year')
|
||||
|
||||
def month(self, field_expr):
|
||||
if hasattr(field_expr, '_django_name'):
|
||||
return _ExtractExpr(field_expr._django_name, 'month')
|
||||
return _ExtractExpr(str(field_expr), 'month')
|
||||
def month(self, field_expr) -> _ExtractExpr:
|
||||
"""提取月份。"""
|
||||
return _ExtractExpr(self._resolve_name(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'):
|
||||
return _ExtractExpr(field_expr._django_name, 'day')
|
||||
return _ExtractExpr(str(field_expr), 'day')
|
||||
return field_expr._django_name
|
||||
if hasattr(field_expr, 'field_name'):
|
||||
return field_expr.field_name
|
||||
return str(field_expr)
|
||||
|
||||
|
||||
func = _CompatFunc()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DELETE 语句构建器
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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.conditions = conditions or Q()
|
||||
|
||||
def where(self, *args, **kwargs):
|
||||
def where(self, *args: Q, **kwargs: Any) -> _DeleteStmt:
|
||||
"""添加 WHERE 条件。"""
|
||||
new_conditions = Q()
|
||||
for a in args:
|
||||
if isinstance(a, Q):
|
||||
new_conditions &= a
|
||||
if kwargs:
|
||||
new_conditions &= Q(**kwargs)
|
||||
self.conditions = new_conditions
|
||||
return self
|
||||
|
||||
def execute(self):
|
||||
def execute(self) -> Tuple[int, Dict[str, int]]:
|
||||
"""执行删除,返回 (删除数, {表名: 删除数})。"""
|
||||
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:
|
||||
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'):
|
||||
_local.db_session = Session()
|
||||
return _local.db_session
|
||||
|
||||
|
||||
def _get_cache():
|
||||
def _get_cache() -> Dict:
|
||||
"""获取当前线程的查询缓存。"""
|
||||
if not hasattr(_local, 'query_cache'):
|
||||
_local.query_cache = {}
|
||||
return _local.query_cache
|
||||
|
||||
|
||||
def _invalidate_cache():
|
||||
def _invalidate_cache() -> None:
|
||||
"""清除当前线程的查询缓存。"""
|
||||
if hasattr(_local, 'query_cache'):
|
||||
_local.query_cache.clear()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 关联字段访问器
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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._prefix = prefix
|
||||
|
||||
def __getattr__(self, name):
|
||||
def __getattr__(self, name: str):
|
||||
if name.startswith('_'):
|
||||
return object.__getattribute__(self, name)
|
||||
try:
|
||||
@@ -163,18 +254,26 @@ class RelationAccessor:
|
||||
return RelationAccessor(rel.related_model, full_prefix)
|
||||
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:
|
||||
"""包装 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:
|
||||
self._qs: models.QuerySet = qs # Django ValuesQuerySet(返回字典)
|
||||
self._fields: List[str] = fields # 转换后的字段名列表(传给 Django .values() 的)
|
||||
self._original_fields: Tuple = original_fields # 原始传入的字段(用于判断单字段场景)
|
||||
self._qs: models.QuerySet = qs
|
||||
self._fields: List[str] = fields
|
||||
self._original_fields: Tuple = original_fields
|
||||
|
||||
@property
|
||||
def query(self) -> Any:
|
||||
@@ -202,12 +301,15 @@ class _ValuesResult:
|
||||
return self
|
||||
|
||||
def first(self) -> Optional[Dict[str, Any]]:
|
||||
"""返回第一条记录(字典)或 None。"""
|
||||
return self._qs.first()
|
||||
|
||||
def all(self) -> List[Dict[str, Any]]:
|
||||
"""返回所有记录的字典列表。"""
|
||||
return list(self._qs)
|
||||
|
||||
def scalar(self) -> Any:
|
||||
"""返回单字段场景的标量值。"""
|
||||
row = self._qs.first()
|
||||
if row is None:
|
||||
return None
|
||||
@@ -237,19 +339,26 @@ class _ValuesResult:
|
||||
def __bool__(self) -> bool:
|
||||
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 查询接口。
|
||||
|
||||
用法示例::
|
||||
由 ``Model.query`` 返回,支持链式调用::
|
||||
|
||||
# 基础查询
|
||||
Dingdan.query.filter(zhuangtai=8).all()
|
||||
|
||||
# 链式过滤
|
||||
Dingdan.query.filter(jine__gt=100).order_by('-create_time').limit(10)
|
||||
Dingdan.query.filter(jine__gt=100).order_by('-CreateTime').limit(10)
|
||||
|
||||
# 字段表达式(通过 Model.field 自动获取 FieldExpression)
|
||||
# 字段表达式
|
||||
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)
|
||||
|
||||
泛型参数 ``_M`` 携带模型类型,使 IDE 能推断链式调用返回的模型实例类型。
|
||||
"""
|
||||
|
||||
def __init__(self, model_class: Type[models.Model]) -> None:
|
||||
self._model: Type[models.Model] = model_class
|
||||
def __init__(self, model_class: Type[_M]) -> None:
|
||||
self._model: Type[_M] = model_class
|
||||
self._qs: models.QuerySet = model_class.objects.all()
|
||||
self._joined_models: List[Type[models.Model]] = []
|
||||
self._cache_enabled: bool = True
|
||||
@@ -273,7 +384,13 @@ class FluentQuery:
|
||||
"""暴露底层 Django QuerySet 的 query 属性,支持 Subquery() 等场景。"""
|
||||
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._qs = self._qs
|
||||
q._joined_models = list(self._joined_models)
|
||||
@@ -281,6 +398,7 @@ class FluentQuery:
|
||||
return q
|
||||
|
||||
def _cache_key(self, method: str) -> Tuple[Any, str, Any]:
|
||||
"""生成缓存键。"""
|
||||
try:
|
||||
query_str = str(self._qs.query)
|
||||
except Exception:
|
||||
@@ -288,6 +406,7 @@ class FluentQuery:
|
||||
return (self._model, method, query_str)
|
||||
|
||||
def _remap_q(self, q: Q) -> Q:
|
||||
"""重映射 Q 对象中的字段名(用于 join 跨表查询)。"""
|
||||
new_children: List[Any] = []
|
||||
for child in q.children:
|
||||
if isinstance(child, Q):
|
||||
@@ -306,6 +425,7 @@ class FluentQuery:
|
||||
return q
|
||||
|
||||
def _remap_key(self, key: str) -> str:
|
||||
"""重映射字段名(用于 join 跨表查询自动添加关联前缀)。"""
|
||||
parts = key.split('__')
|
||||
field_name = parts[0]
|
||||
try:
|
||||
@@ -330,30 +450,36 @@ class FluentQuery:
|
||||
pass
|
||||
return key
|
||||
|
||||
def filter(self, *args: Any, **kwargs: Any) -> FluentQuery:
|
||||
"""过滤查询,支持 Q 对象和关键字参数,返回 self 以支持链式调用。"""
|
||||
# ---- 过滤 ----
|
||||
|
||||
def filter(self, *args: Any, **kwargs: Any) -> FluentQuery[_M]:
|
||||
"""过滤查询,支持 Q 对象和关键字参数;返回新 FluentQuery,不修改原查询。"""
|
||||
converted_args: List[Any] = []
|
||||
for arg in args:
|
||||
if isinstance(arg, Q) and self._joined_models:
|
||||
converted_args.append(self._remap_q(arg))
|
||||
else:
|
||||
converted_args.append(arg)
|
||||
self._qs = self._qs.filter(*converted_args, **kwargs)
|
||||
return self
|
||||
q = self._clone()
|
||||
q._qs = self._qs.filter(*converted_args, **kwargs)
|
||||
return q
|
||||
|
||||
def exclude(self, *args: Any, **kwargs: Any) -> FluentQuery:
|
||||
"""排除查询,返回 self 以支持链式调用。"""
|
||||
def exclude(self, *args: Any, **kwargs: Any) -> FluentQuery[_M]:
|
||||
"""排除查询;返回新 FluentQuery,不修改原查询。"""
|
||||
converted_args: List[Any] = []
|
||||
for arg in args:
|
||||
if isinstance(arg, Q) and self._joined_models:
|
||||
converted_args.append(self._remap_q(arg))
|
||||
else:
|
||||
converted_args.append(arg)
|
||||
self._qs = self._qs.exclude(*converted_args, **kwargs)
|
||||
return self
|
||||
q = self._clone()
|
||||
q._qs = self._qs.exclude(*converted_args, **kwargs)
|
||||
return q
|
||||
|
||||
def order_by(self, *args: Any) -> FluentQuery:
|
||||
"""排序,支持字符串和 FieldExpression,返回 self 以支持链式调用。"""
|
||||
# ---- 排序 ----
|
||||
|
||||
def order_by(self, *args: Any) -> FluentQuery[_M]:
|
||||
"""排序,支持字符串和 FieldExpression;返回新 FluentQuery,不修改原查询。"""
|
||||
converted: List[str] = []
|
||||
for a in args:
|
||||
if isinstance(a, str):
|
||||
@@ -362,10 +488,13 @@ class FluentQuery:
|
||||
converted.append(str(a))
|
||||
else:
|
||||
converted.append(str(a))
|
||||
self._qs = self._qs.order_by(*converted)
|
||||
return self
|
||||
q = self._clone()
|
||||
q._qs = q._qs.order_by(*converted)
|
||||
return q
|
||||
|
||||
def first(self) -> Optional[models.Model]:
|
||||
# ---- 获取记录 ----
|
||||
|
||||
def first(self) -> Optional[_M]:
|
||||
"""返回第一条记录或 None。"""
|
||||
if self._cache_enabled:
|
||||
cache = _get_cache()
|
||||
@@ -379,13 +508,26 @@ class FluentQuery:
|
||||
_get_cache()[self._cache_key('first')] = 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() 一致)。
|
||||
如需获取列表,使用 .to_list() 或 list()。"""
|
||||
如需获取列表,使用 ``.to_list()`` 或 ``list()``。"""
|
||||
self._qs = self._qs.all()
|
||||
return self
|
||||
|
||||
def to_list(self) -> List[models.Model]:
|
||||
def to_list(self) -> List[_M]:
|
||||
"""执行查询并返回模型实例列表。"""
|
||||
if self._cache_enabled:
|
||||
cache = _get_cache()
|
||||
@@ -412,93 +554,37 @@ class FluentQuery:
|
||||
_get_cache()[self._cache_key('count')] = result
|
||||
return result
|
||||
|
||||
def get(self, *args: Any, **kwargs: Any) -> models.Model:
|
||||
"""获取唯一匹配记录,不存在或多个时抛异常。"""
|
||||
if args:
|
||||
obj = self._qs.filter(*args).get(**kwargs)
|
||||
else:
|
||||
obj = self._qs.get(**kwargs)
|
||||
_get_session().track(obj)
|
||||
return obj
|
||||
def exists(self) -> bool:
|
||||
"""判断是否存在匹配记录。"""
|
||||
return self._qs.exists()
|
||||
|
||||
def create(self, **kwargs: Any) -> models.Model:
|
||||
# ---- 创建/更新/删除 ----
|
||||
|
||||
def create(self, **kwargs: Any) -> _M:
|
||||
"""创建并保存新记录。"""
|
||||
obj = self._model.objects.create(**kwargs)
|
||||
_invalidate_cache()
|
||||
return obj
|
||||
|
||||
def get_or_create(self, defaults: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Tuple[models.Model, bool]:
|
||||
"""查询或创建记录,返回 (obj, created) 元组。"""
|
||||
def get_or_create(self, defaults: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any) -> Tuple[_M, bool]:
|
||||
"""查询或创建记录,返回 ``(obj, created)`` 元组。"""
|
||||
obj, created = self._model.objects.get_or_create(defaults=defaults, **kwargs)
|
||||
if created:
|
||||
_invalidate_cache()
|
||||
_get_session().track(obj)
|
||||
return obj, created
|
||||
|
||||
def update_or_create(self, defaults: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Tuple[models.Model, bool]:
|
||||
"""更新或创建记录,返回 (obj, created) 元组。"""
|
||||
def update_or_create(self, defaults: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any) -> Tuple[_M, bool]:
|
||||
"""更新或创建记录,返回 ``(obj, created)`` 元组。"""
|
||||
obj, created = self._model.objects.update_or_create(defaults=defaults, **kwargs)
|
||||
_invalidate_cache()
|
||||
_get_session().track(obj)
|
||||
return obj, created
|
||||
|
||||
def none(self) -> FluentQuery:
|
||||
"""返回空结果集,返回 self 以支持链式调用。"""
|
||||
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]:
|
||||
def bulk_create(self, objs: List[_M], batch_size: Optional[int] = None,
|
||||
ignore_conflicts: bool = False) -> List[_M]:
|
||||
"""批量创建记录。"""
|
||||
result = self._model.objects.bulk_create(
|
||||
objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts
|
||||
@@ -506,19 +592,116 @@ class FluentQuery:
|
||||
_invalidate_cache()
|
||||
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._qs = self._qs.annotate(**kwargs)
|
||||
return self
|
||||
|
||||
def aggregate(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
"""聚合查询,返回聚合结果字典。"""
|
||||
return self._qs.aggregate(**kwargs)
|
||||
def aggregate(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||
"""聚合查询,返回聚合结果字典。
|
||||
|
||||
兼容 Django QuerySet.aggregate() 的两种调用方式:
|
||||
aggregate(Max('field')) # 位置参数
|
||||
aggregate(field__max=Max('field')) # 关键字参数
|
||||
"""
|
||||
return self._qs.aggregate(*args, **kwargs)
|
||||
|
||||
def values(self, *fields: Union[str, _AggregateExpr, models.Expression]) -> _ValuesResult:
|
||||
"""返回指定字段的字典结果,支持链式调用。"""
|
||||
if not fields:
|
||||
# 无参数:返回 Django .values()(所有字段字典)
|
||||
return _ValuesResult(self._qs.values(), [], fields)
|
||||
|
||||
annotations: Dict[str, models.Aggregate] = {}
|
||||
@@ -538,7 +721,6 @@ class FluentQuery:
|
||||
qs = self._qs
|
||||
if annotations:
|
||||
qs = qs.annotate(**annotations)
|
||||
# 使用 Django 原生 .values() 返回字典
|
||||
qs = qs.values(*converted)
|
||||
return _ValuesResult(qs, converted, fields)
|
||||
|
||||
@@ -546,38 +728,16 @@ class FluentQuery:
|
||||
"""values() 的别名,SQLAlchemy 风格。"""
|
||||
return self.values(*fields)
|
||||
|
||||
def params(self, **kwargs: Any) -> FluentQuery:
|
||||
"""参数占位(SQLAlchemy 兼容),返回 self 以支持链式调用。"""
|
||||
return self
|
||||
|
||||
def values_list(self, *fields: str, flat: bool = False) -> Any:
|
||||
"""返回指定字段的元组列表。"""
|
||||
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]]:
|
||||
"""批量删除匹配记录,返回 (删除数, {表名: 删除数})。"""
|
||||
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:
|
||||
def paginate(self, page: int = 1, per_page: int = 20,
|
||||
error_out: bool = False) -> Any:
|
||||
"""分页查询,返回 Page 对象(含 items, total, pages 等属性)。"""
|
||||
from django.core.paginator import Paginator
|
||||
from django.core.paginator import Paginator, EmptyPage
|
||||
if not self._qs.ordered:
|
||||
meta = getattr(self._model, '_meta', None)
|
||||
if meta and meta.pk:
|
||||
@@ -585,7 +745,7 @@ class FluentQuery:
|
||||
paginator = Paginator(self._qs, per_page)
|
||||
try:
|
||||
page_obj = paginator.page(page)
|
||||
except Exception:
|
||||
except EmptyPage:
|
||||
page_obj = paginator.page(1)
|
||||
page_obj.total = paginator.count
|
||||
page_obj.items = page_obj.object_list
|
||||
@@ -598,26 +758,17 @@ class FluentQuery:
|
||||
page_obj.prev_num = page_obj.previous_page_number() if page_obj.has_prev else None
|
||||
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:
|
||||
"""支持切片 [start:end] 和索引 [n]。切片返回 self,索引返回模型实例。"""
|
||||
"""支持切片 ``[start:end]`` 和索引 ``[n]``。切片返回 self,索引返回模型实例。"""
|
||||
if isinstance(key, slice):
|
||||
self._qs = self._qs[key]
|
||||
return self
|
||||
else:
|
||||
# 单个索引,返回模型实例
|
||||
return self._qs[key]
|
||||
|
||||
def __iter__(self) -> Iterator[models.Model]:
|
||||
def __iter__(self) -> Iterator[_M]:
|
||||
return iter(self._qs)
|
||||
|
||||
def __len__(self) -> int:
|
||||
@@ -630,25 +781,43 @@ class FluentQuery:
|
||||
return f'FluentQuery({self._model.__name__})'
|
||||
|
||||
|
||||
class Session:
|
||||
def __init__(self):
|
||||
self._pending_adds = []
|
||||
self._pending_deletes = []
|
||||
self._tracked = set()
|
||||
# ---------------------------------------------------------------------------
|
||||
# Session 会话管理
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
|
||||
def delete(self, obj):
|
||||
def delete(self, obj: models.Model) -> None:
|
||||
"""添加对象到待删除列表。"""
|
||||
self._pending_deletes.append(obj)
|
||||
self._tracked.discard(obj)
|
||||
|
||||
def track(self, obj):
|
||||
def track(self, obj: models.Model) -> None:
|
||||
"""跟踪对象变更(commit 时自动 save)。"""
|
||||
if isinstance(obj, models.Model):
|
||||
self._tracked.add(obj)
|
||||
|
||||
def commit(self):
|
||||
delete_pks = {(obj.__class__, obj.pk) for obj in self._pending_deletes if obj.pk is not None}
|
||||
def commit(self) -> None:
|
||||
"""提交所有待处理操作(删除 → 新增 → 更新)。"""
|
||||
delete_pks = {(obj.__class__, obj.pk) for obj in self._pending_deletes
|
||||
if obj.pk is not None}
|
||||
with transaction.atomic():
|
||||
for obj in self._pending_deletes:
|
||||
obj.delete()
|
||||
@@ -660,21 +829,23 @@ class Session:
|
||||
try:
|
||||
obj.save()
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug('Session.commit: skip tracked obj %s', obj, exc_info=True)
|
||||
self._pending_adds.clear()
|
||||
self._pending_deletes.clear()
|
||||
self._tracked.clear()
|
||||
_invalidate_cache()
|
||||
|
||||
def rollback(self):
|
||||
def rollback(self) -> None:
|
||||
"""回滚所有待处理操作。"""
|
||||
self._pending_adds.clear()
|
||||
self._pending_deletes.clear()
|
||||
self._tracked.clear()
|
||||
_invalidate_cache()
|
||||
|
||||
def query(self, *args):
|
||||
model = None
|
||||
aggregates = []
|
||||
def query(self, *args) -> Optional[FluentQuery[Any]]:
|
||||
"""创建查询构建器,支持传入模型类或聚合表达式。"""
|
||||
model: Optional[Type[models.Model]] = None
|
||||
aggregates: List = []
|
||||
for a in args:
|
||||
if isinstance(a, type) and issubclass(a, models.Model):
|
||||
model = a
|
||||
@@ -691,10 +862,12 @@ class Session:
|
||||
return fq.values(*args)
|
||||
return None
|
||||
|
||||
def flush(self):
|
||||
def flush(self) -> None:
|
||||
"""flush() 的别名,等同于 commit()。"""
|
||||
self.commit()
|
||||
|
||||
def execute(self, stmt):
|
||||
def execute(self, stmt) -> None:
|
||||
"""执行语句(支持 _DeleteStmt 和原始 SQL 字符串)。"""
|
||||
if isinstance(stmt, _DeleteStmt):
|
||||
stmt.execute()
|
||||
elif isinstance(stmt, str):
|
||||
@@ -703,14 +876,31 @@ class Session:
|
||||
cursor.execute(stmt)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 全局入口
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
def session(self):
|
||||
def session(self) -> Session:
|
||||
"""获取当前线程的 Session 实例。"""
|
||||
return _get_session()
|
||||
|
||||
@staticmethod
|
||||
def or_(*args):
|
||||
result = None
|
||||
def or_(*args: Q) -> Q:
|
||||
"""逻辑 OR,合并多个 Q 对象。"""
|
||||
result: Optional[Q] = None
|
||||
for q in args:
|
||||
if isinstance(q, Q):
|
||||
if result is None:
|
||||
@@ -720,7 +910,8 @@ class DB:
|
||||
return result if result is not None else Q()
|
||||
|
||||
@staticmethod
|
||||
def and_(*args):
|
||||
def and_(*args: Q) -> Q:
|
||||
"""逻辑 AND,合并多个 Q 对象。"""
|
||||
result = Q()
|
||||
for q in args:
|
||||
if isinstance(q, Q):
|
||||
@@ -731,5 +922,6 @@ class DB:
|
||||
db = DB()
|
||||
|
||||
|
||||
def FQ(model_class):
|
||||
def FQ(model_class: Type[_M]) -> FluentQuery[_M]:
|
||||
"""FluentQuery 工厂函数,等同于 ``Model.query``。"""
|
||||
return FluentQuery(model_class)
|
||||
|
||||
30
gvsdsdk/management/commands/hash_passwords.py
Normal file
30
gvsdsdk/management/commands/hash_passwords.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""将 User 表中明文密码迁移为 bcrypt 哈希"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '将 User 表中所有明文密码迁移为 bcrypt 哈希'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# 筛选非空且非 bcrypt 哈希的密码
|
||||
users = User.objects.exclude(UserPassword='')
|
||||
updated = 0
|
||||
skipped = 0
|
||||
|
||||
for user in users.iterator():
|
||||
stored = user.UserPassword or ''
|
||||
# 已是 bcrypt 哈希则跳过
|
||||
if stored.startswith(('$2b$', '$2a$')) and len(stored) == 60:
|
||||
skipped += 1
|
||||
continue
|
||||
# 明文密码 → bcrypt 哈希
|
||||
user.SetPassword(stored)
|
||||
user.save(update_fields=['UserPassword'])
|
||||
updated += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'完成:{updated} 条明文密码已转为 bcrypt,{skipped} 条已是哈希跳过'
|
||||
))
|
||||
@@ -1,8 +1,8 @@
|
||||
# Generated by Django 6.0.5 on 2026-06-13 11:43
|
||||
# Generated by Django 4.2.27 on 2026-06-19 18:42
|
||||
|
||||
import datetime
|
||||
import gvsdsdk.models
|
||||
from django.db import migrations, models
|
||||
import gvsdsdk.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -16,9 +16,9 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='AccountActivation',
|
||||
fields=[
|
||||
('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False, verbose_name='激活码')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '账户激活',
|
||||
@@ -30,13 +30,13 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='EphemeralToken',
|
||||
fields=[
|
||||
('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False)),
|
||||
('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True)),
|
||||
('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow)),
|
||||
('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._elt_end_time_default)),
|
||||
('ELTType', models.SmallIntegerField(db_column='ELTType', default=0)),
|
||||
('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list)),
|
||||
('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1)),
|
||||
('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False, verbose_name='令牌UUID')),
|
||||
('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True, verbose_name='用户UUID')),
|
||||
('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._EltEndTimeDefault, verbose_name='过期时间')),
|
||||
('ELTType', models.SmallIntegerField(db_column='ELTType', default=0, verbose_name='令牌类型')),
|
||||
('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list, verbose_name='允许IP')),
|
||||
('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1, verbose_name='状态')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '临时令牌',
|
||||
@@ -48,8 +48,8 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='Password',
|
||||
fields=[
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
|
||||
('Password', models.CharField(db_column='Password', max_length=1024)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False, verbose_name='用户UUID')),
|
||||
('Password', models.CharField(db_column='Password', max_length=1024, verbose_name='密码哈希')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '密码',
|
||||
@@ -61,12 +61,14 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='Permission',
|
||||
fields=[
|
||||
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False)),
|
||||
('PermName', models.CharField(db_column='PermName', max_length=128)),
|
||||
('PermCode', models.CharField(db_column='PermCode', max_length=128)),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
||||
('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1)),
|
||||
('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512)),
|
||||
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False, verbose_name='权限UUID')),
|
||||
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||
('PermName', models.CharField(db_column='PermName', max_length=128, verbose_name='权限名称')),
|
||||
('PermCode', models.CharField(db_column='PermCode', max_length=128, verbose_name='权限键')),
|
||||
('PermCategory', models.CharField(db_column='PermCategory', default='', max_length=64, verbose_name='权限分类')),
|
||||
('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1, verbose_name='是否启用')),
|
||||
('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512, verbose_name='描述')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '权限',
|
||||
@@ -75,16 +77,72 @@ class Migration(migrations.Migration):
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Position',
|
||||
fields=[
|
||||
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID', primary_key=True, serialize=False, verbose_name='职务UUID')),
|
||||
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||
('PositionName', models.CharField(db_column='PositionName', max_length=128, verbose_name='职务名称')),
|
||||
('PositionCode', models.CharField(db_column='PositionCode', default='', max_length=64, verbose_name='职务编码')),
|
||||
('PositionLevel', models.SmallIntegerField(db_column='PositionLevel', default=0, verbose_name='职务级别')),
|
||||
('PositionStatus', models.SmallIntegerField(db_column='PositionStatus', default=1, verbose_name='状态')),
|
||||
('PositionDesc', models.CharField(db_column='PositionDesc', default='', max_length=512, verbose_name='描述')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '人事职务标签',
|
||||
'verbose_name_plural': '人事职务标签',
|
||||
'db_table': 'Position',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PositionTemplate',
|
||||
fields=[
|
||||
('PositionTemplateUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionTemplateUUID', primary_key=True, serialize=False, verbose_name='岗位模板UUID')),
|
||||
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||
('TemplateName', models.CharField(db_column='TemplateName', max_length=128, verbose_name='模板名称')),
|
||||
('TemplateCode', models.CharField(db_column='TemplateCode', default='', max_length=64, verbose_name='模板编码')),
|
||||
('TemplateStatus', models.SmallIntegerField(db_column='TemplateStatus', default=1, verbose_name='模板状态')),
|
||||
('TemplateDesc', models.CharField(db_column='TemplateDesc', default='', max_length=512, verbose_name='模板描述')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '岗位模板',
|
||||
'verbose_name_plural': '岗位模板',
|
||||
'db_table': 'PositionTemplate',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PositionTemplatePermission',
|
||||
fields=[
|
||||
('PTPermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PTPermUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||
('PositionTemplateUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionTemplateUUID', verbose_name='岗位模板UUID')),
|
||||
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', verbose_name='权限UUID')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '岗位模板权限关联',
|
||||
'verbose_name_plural': '岗位模板权限关联',
|
||||
'db_table': 'PositionTemplatePermission',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False)),
|
||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')),
|
||||
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='DeptUUID', null=True)),
|
||||
('RoleName', models.CharField(db_column='RoleName', max_length=128)),
|
||||
('RoleType', models.SmallIntegerField(db_column='RoleType', default=2)),
|
||||
('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1)),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False, verbose_name='角色UUID')),
|
||||
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||
('RoleName', models.CharField(db_column='RoleName', max_length=128, verbose_name='角色名称')),
|
||||
('RoleType', models.SmallIntegerField(db_column='RoleType', default=2, verbose_name='角色类型')),
|
||||
('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1, verbose_name='角色状态')),
|
||||
('AssignScope', models.SmallIntegerField(db_column='AssignScope', default=0, verbose_name='分配可见范围')),
|
||||
('AssignScopeUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='AssignScopeUUID', null=True, verbose_name='分配范围UUID')),
|
||||
('RoleDesc', models.CharField(db_column='RoleDesc', default='', max_length=512, verbose_name='角色描述')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '角色',
|
||||
@@ -94,55 +152,69 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RoleDept',
|
||||
name='RolePermission',
|
||||
fields=[
|
||||
('RoleDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleDeptUUID', primary_key=True, serialize=False)),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
||||
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID')),
|
||||
('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1)),
|
||||
('RolePermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RolePermUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
|
||||
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', verbose_name='权限UUID')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '角色部门关联',
|
||||
'verbose_name_plural': '角色部门关联',
|
||||
'db_table': 'RoleDept',
|
||||
'verbose_name': '角色权限关联',
|
||||
'verbose_name_plural': '角色权限关联',
|
||||
'db_table': 'RolePermission',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
name='RoleShop',
|
||||
fields=[
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
|
||||
('UserName', models.CharField(db_column='UserName', max_length=256, unique=True)),
|
||||
('UserPassword', models.CharField(db_column='UserPassword', default='', max_length=60)),
|
||||
('UserPositionStatus', models.SmallIntegerField(db_column='UserPositionStatus', default=0)),
|
||||
('UserPersonalSignature', models.CharField(db_column='UserPersonalSignature', default='', max_length=1024)),
|
||||
('UserEmail', models.CharField(db_column='UserEmail', default='', max_length=256)),
|
||||
('UserEmailVerify', models.SmallIntegerField(db_column='UserEmailVerify', default=0)),
|
||||
('UserAccountLicense', models.SmallIntegerField(db_column='UserAccountLicense', default=1)),
|
||||
('UserGender', models.SmallIntegerField(db_column='UserGender', default=-1)),
|
||||
('UserTele', models.CharField(blank=True, db_column='UserTele', max_length=16, null=True)),
|
||||
('UserCreateTime', models.DateTimeField(db_column='UserCreateTime', default=datetime.datetime.utcnow)),
|
||||
('UserLastLoginDate', models.DateTimeField(db_column='UserLastLoginDate', default=datetime.datetime.utcnow)),
|
||||
('IsStaff', models.BooleanField(db_column='IsStaff', default=False)),
|
||||
('IsSuperuser', models.BooleanField(db_column='IsSuperuser', default=False)),
|
||||
('RoleShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleShopUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
|
||||
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', verbose_name='商铺UUID')),
|
||||
('IsMainShop', models.SmallIntegerField(db_column='IsMainShop', default=0, verbose_name='主商铺')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户',
|
||||
'verbose_name_plural': '用户',
|
||||
'db_table': 'User',
|
||||
'verbose_name': '角色商铺关联',
|
||||
'verbose_name_plural': '角色商铺关联',
|
||||
'db_table': 'RoleShop',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Shop',
|
||||
fields=[
|
||||
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', primary_key=True, serialize=False, verbose_name='商铺UUID')),
|
||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID', verbose_name='公司UUID')),
|
||||
('ShopName', models.CharField(db_column='ShopName', max_length=128, verbose_name='商铺名称')),
|
||||
('ShopCode', models.CharField(db_column='ShopCode', default='', max_length=64, verbose_name='商铺编码')),
|
||||
('ShopType', models.SmallIntegerField(db_column='ShopType', default=0, verbose_name='商铺类型')),
|
||||
('ShopStatus', models.SmallIntegerField(db_column='ShopStatus', default=1, verbose_name='商铺状态')),
|
||||
('ShopAddress', models.CharField(db_column='ShopAddress', default='', max_length=512, verbose_name='商铺地址')),
|
||||
('ShopContact', models.CharField(db_column='ShopContact', default='', max_length=128, verbose_name='联系人')),
|
||||
('ShopPhone', models.CharField(db_column='ShopPhone', default='', max_length=32, verbose_name='联系电话')),
|
||||
('ShopDesc', models.CharField(db_column='ShopDesc', default='', max_length=512, verbose_name='描述')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '商铺',
|
||||
'verbose_name_plural': '商铺',
|
||||
'db_table': 'Shop',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='User2FA',
|
||||
fields=[
|
||||
('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)),
|
||||
('SecretKey', models.CharField(db_column='SecretKey', max_length=64)),
|
||||
('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0)),
|
||||
('Verified', models.SmallIntegerField(db_column='Verified', default=0)),
|
||||
('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True)),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False, verbose_name='2FA UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True, verbose_name='用户UUID')),
|
||||
('SecretKey', models.CharField(db_column='SecretKey', max_length=64, verbose_name='密钥')),
|
||||
('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0, verbose_name='是否启用')),
|
||||
('Verified', models.SmallIntegerField(db_column='Verified', default=0, verbose_name='已验证')),
|
||||
('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True, verbose_name='最后使用')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '双因素认证',
|
||||
@@ -154,9 +226,9 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='UserABAC',
|
||||
fields=[
|
||||
('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
|
||||
('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256)),
|
||||
('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False, verbose_name='ABAC UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||
('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256, verbose_name='层级权限标识')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户ABAC',
|
||||
@@ -168,10 +240,10 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='UserAvatar',
|
||||
fields=[
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
|
||||
('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True)),
|
||||
('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow)),
|
||||
('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False, verbose_name='用户UUID')),
|
||||
('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True, verbose_name='头像数据')),
|
||||
('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow, verbose_name='上传时间')),
|
||||
('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16, verbose_name='文件类型')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户头像',
|
||||
@@ -180,16 +252,33 @@ class Migration(migrations.Migration):
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserDept',
|
||||
fields=[
|
||||
('UserDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserDeptUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID', verbose_name='部门UUID')),
|
||||
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='PositionUUID', null=True, verbose_name='职务标签UUID')),
|
||||
('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1, verbose_name='是否主部门')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '员工部门关联',
|
||||
'verbose_name_plural': '员工部门关联',
|
||||
'db_table': 'UserDept',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserLoginLog',
|
||||
fields=[
|
||||
('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)),
|
||||
('LoginIP', models.CharField(db_column='LoginIP', max_length=39)),
|
||||
('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32)),
|
||||
('LoginStatus', models.SmallIntegerField(db_column='LoginStatus')),
|
||||
('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow)),
|
||||
('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512)),
|
||||
('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False, verbose_name='日志UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True, verbose_name='用户UUID')),
|
||||
('LoginIP', models.CharField(db_column='LoginIP', max_length=39, verbose_name='登录IP')),
|
||||
('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32, verbose_name='登录平台')),
|
||||
('LoginStatus', models.SmallIntegerField(db_column='LoginStatus', verbose_name='登录状态')),
|
||||
('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow, verbose_name='登录时间')),
|
||||
('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512, verbose_name='用户代理')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '登录日志',
|
||||
@@ -201,10 +290,10 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='UserPending2FA',
|
||||
fields=[
|
||||
('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True)),
|
||||
('SecretKey', models.CharField(db_column='SecretKey', max_length=64)),
|
||||
('ExpireAt', models.DateTimeField(db_column='ExpireAt')),
|
||||
('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False, verbose_name='待确认UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True, verbose_name='用户UUID')),
|
||||
('SecretKey', models.CharField(db_column='SecretKey', max_length=64, verbose_name='密钥')),
|
||||
('ExpireAt', models.DateTimeField(db_column='ExpireAt', verbose_name='过期时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '2FA待确认',
|
||||
@@ -216,10 +305,11 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='UserRole',
|
||||
fields=[
|
||||
('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
|
||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID', verbose_name='公司UUID')),
|
||||
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户角色关联',
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
# Generated by Django 6.0.5 on 2026-06-13 12:29
|
||||
|
||||
import datetime
|
||||
import gvsdsdk.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gvsdsdk', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Position',
|
||||
fields=[
|
||||
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID', primary_key=True, serialize=False)),
|
||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')),
|
||||
('PositionName', models.CharField(db_column='PositionName', max_length=128)),
|
||||
('PositionCode', models.CharField(db_column='PositionCode', default='', max_length=64)),
|
||||
('PositionLevel', models.SmallIntegerField(db_column='PositionLevel', default=0)),
|
||||
('PositionStatus', models.SmallIntegerField(db_column='PositionStatus', default=1)),
|
||||
('PositionDesc', models.CharField(db_column='PositionDesc', default='', max_length=512)),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '职务',
|
||||
'verbose_name_plural': '职务',
|
||||
'db_table': 'Position',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RolePosition',
|
||||
fields=[
|
||||
('RolePositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RolePositionUUID', primary_key=True, serialize=False)),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
||||
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID')),
|
||||
('IsMainPosition', models.SmallIntegerField(db_column='IsMainPosition', default=0)),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '角色职务关联',
|
||||
'verbose_name_plural': '角色职务关联',
|
||||
'db_table': 'RolePosition',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RoleShop',
|
||||
fields=[
|
||||
('RoleShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleShopUUID', primary_key=True, serialize=False)),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
||||
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID')),
|
||||
('IsMainShop', models.SmallIntegerField(db_column='IsMainShop', default=0)),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '角色商铺关联',
|
||||
'verbose_name_plural': '角色商铺关联',
|
||||
'db_table': 'RoleShop',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Shop',
|
||||
fields=[
|
||||
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', primary_key=True, serialize=False)),
|
||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')),
|
||||
('ShopName', models.CharField(db_column='ShopName', max_length=128)),
|
||||
('ShopCode', models.CharField(db_column='ShopCode', default='', max_length=64)),
|
||||
('ShopType', models.SmallIntegerField(db_column='ShopType', default=0)),
|
||||
('ShopStatus', models.SmallIntegerField(db_column='ShopStatus', default=1)),
|
||||
('ShopAddress', models.CharField(db_column='ShopAddress', default='', max_length=512)),
|
||||
('ShopContact', models.CharField(db_column='ShopContact', default='', max_length=128)),
|
||||
('ShopPhone', models.CharField(db_column='ShopPhone', default='', max_length=32)),
|
||||
('ShopDesc', models.CharField(db_column='ShopDesc', default='', max_length=512)),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '商铺',
|
||||
'verbose_name_plural': '商铺',
|
||||
'db_table': 'Shop',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,58 +1,86 @@
|
||||
"""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('-CreateTime').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
|
||||
|
||||
import typing
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
||||
import re
|
||||
from typing import Any, Callable, Dict, Generic, Optional, Tuple, Type, TypeVar, Union
|
||||
|
||||
try:
|
||||
from typing import Self
|
||||
except ImportError:
|
||||
from typing_extensions import Self
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
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
|
||||
|
||||
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))
|
||||
|
||||
|
||||
ABBREVIATIONS = {
|
||||
'uuid': 'UUID',
|
||||
'id': 'ID',
|
||||
'ip': 'IP',
|
||||
'url': 'URL',
|
||||
'uri': 'URI',
|
||||
'api': 'API',
|
||||
'html': 'HTML',
|
||||
'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',
|
||||
ABBREVIATIONS: Dict[str, str] = {
|
||||
'uuid': 'UUID', 'id': 'ID', 'ip': 'IP', 'url': 'URL', 'uri': 'URI',
|
||||
'api': 'API', 'html': 'HTML', '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:
|
||||
return name[0].upper() + name[1:] if name else name
|
||||
parts = name.split('_')
|
||||
@@ -66,46 +94,74 @@ def snake_to_pascal(name):
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def pascal_to_snake(name):
|
||||
import re
|
||||
def pascal_to_snake(name: str) -> str:
|
||||
"""将 PascalCase 转换为 snake_case。
|
||||
|
||||
>>> pascal_to_snake('UserUUID')
|
||||
'user_uuid'
|
||||
"""
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1)
|
||||
parts = s2.lower().split('_')
|
||||
return '_'.join(parts)
|
||||
return s2.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 字段表达式(类级别访问 Model.field 时返回)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _InvertedFieldExpression:
|
||||
def __init__(self, field_expr):
|
||||
"""取反字段表达式,由 ``~Model.field`` 产生,支持 ``~field.startswith(...)`` 等否定查询。"""
|
||||
|
||||
def __init__(self, field_expr: FieldExpression) -> None:
|
||||
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})
|
||||
|
||||
def contains(self, value):
|
||||
def contains(self, value: str) -> Q:
|
||||
return ~Q(**{f'{self._field_expr._django_name}__contains': value})
|
||||
|
||||
def like(self, pattern):
|
||||
def like(self, pattern: str) -> Q:
|
||||
clean = pattern.strip('%')
|
||||
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})
|
||||
|
||||
def is_(self, other):
|
||||
def is_(self, other) -> Q:
|
||||
if other is None:
|
||||
return ~Q(**{f'{self._field_expr._django_name}__isnull': True})
|
||||
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:
|
||||
_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.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)
|
||||
if cache_key in FieldExpression._resolve_cache:
|
||||
return FieldExpression._resolve_cache[cache_key]
|
||||
@@ -117,243 +173,232 @@ class FieldExpression:
|
||||
FieldExpression._resolve_cache[cache_key] = self.field_name
|
||||
return self.field_name
|
||||
|
||||
def __eq__(self, other):
|
||||
# ---- 比较运算符 ----
|
||||
|
||||
def __eq__(self, other) -> Q: # type: ignore[override]
|
||||
if isinstance(other, FieldExpression):
|
||||
return NotImplemented
|
||||
if other is None:
|
||||
return Q(**{f'{self._django_name}__isnull': True})
|
||||
return Q(**{self._django_name: other})
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other) -> Q: # type: ignore[override]
|
||||
if isinstance(other, FieldExpression):
|
||||
return NotImplemented
|
||||
return ~Q(**{self._django_name: other})
|
||||
|
||||
def __gt__(self, other):
|
||||
def __gt__(self, other) -> Q:
|
||||
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})
|
||||
|
||||
def __lt__(self, other):
|
||||
def __lt__(self, other) -> Q:
|
||||
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})
|
||||
|
||||
def __getitem__(self, key):
|
||||
return _JsonKeyExpression(self._django_name, key)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.model_class.__name__, self.field_name))
|
||||
|
||||
def __and__(self, other):
|
||||
# ---- 逻辑运算符 ----
|
||||
|
||||
def __and__(self, other) -> Q:
|
||||
if isinstance(other, Q):
|
||||
return Q(self) & other
|
||||
if isinstance(other, FieldExpression):
|
||||
return Q(self) & Q(other)
|
||||
return NotImplemented
|
||||
|
||||
def __or__(self, other):
|
||||
def __or__(self, other) -> Q:
|
||||
if isinstance(other, Q):
|
||||
return Q(self) | other
|
||||
if isinstance(other, FieldExpression):
|
||||
return Q(self) | Q(other)
|
||||
return NotImplemented
|
||||
|
||||
def __invert__(self):
|
||||
def __invert__(self) -> _InvertedFieldExpression:
|
||||
return _InvertedFieldExpression(self)
|
||||
|
||||
def is_(self, other):
|
||||
# ---- 查询方法 ----
|
||||
|
||||
def is_(self, other) -> Q:
|
||||
"""``field IS NULL`` 或 ``field IS value``。"""
|
||||
if other is None:
|
||||
return Q(**{f'{self._django_name}__isnull': True})
|
||||
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:
|
||||
return Q(**{f'{self._django_name}__isnull': False})
|
||||
return ~Q(**{self._django_name: other})
|
||||
|
||||
def like(self, pattern):
|
||||
def like(self, pattern: str) -> Q:
|
||||
"""模糊匹配(不区分大小写),自动去除 ``%`` 通配符。"""
|
||||
clean = pattern.strip('%')
|
||||
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})
|
||||
|
||||
def ilike(self, pattern):
|
||||
def ilike(self, pattern: str) -> Q:
|
||||
"""like() 的别名。"""
|
||||
return self.like(pattern)
|
||||
|
||||
def startswith(self, value):
|
||||
def startswith(self, value: str) -> Q:
|
||||
"""前缀匹配(区分大小写)。"""
|
||||
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})
|
||||
|
||||
def endswith(self, value):
|
||||
def endswith(self, value: str) -> Q:
|
||||
"""后缀匹配(区分大小写)。"""
|
||||
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})
|
||||
|
||||
def in_(self, values):
|
||||
def in_(self, values) -> Q:
|
||||
"""``field IN (values)``,支持列表和 FluentQuery 子查询。"""
|
||||
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})
|
||||
|
||||
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})
|
||||
|
||||
def regex(self, pattern):
|
||||
def regex(self, pattern: str) -> Q:
|
||||
"""正则匹配(区分大小写)。"""
|
||||
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})
|
||||
|
||||
def desc(self):
|
||||
# ---- 排序辅助 ----
|
||||
|
||||
def desc(self) -> str:
|
||||
"""返回降序排序字符串 ``'-field_name'``。"""
|
||||
return f'-{self._django_name}'
|
||||
|
||||
def asc(self):
|
||||
def asc(self) -> str:
|
||||
"""返回升序排序字符串 ``'field_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})'
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self._django_name
|
||||
|
||||
|
||||
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.key = key
|
||||
|
||||
def _lookup(self, suffix=''):
|
||||
def _lookup(self, suffix: str = '') -> str:
|
||||
if suffix:
|
||||
return f'{self.field_name}__{self.key}__{suffix}'
|
||||
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)
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> Q: # type: ignore[override]
|
||||
return Q(**{self._lookup(): other})
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other) -> Q: # type: ignore[override]
|
||||
return ~Q(**{self._lookup(): other})
|
||||
|
||||
def __gt__(self, other):
|
||||
def __gt__(self, other) -> Q:
|
||||
return Q(**{self._lookup('gt'): other})
|
||||
|
||||
def __ge__(self, other):
|
||||
def __ge__(self, other) -> Q:
|
||||
return Q(**{self._lookup('gte'): other})
|
||||
|
||||
def __lt__(self, other):
|
||||
def __lt__(self, other) -> Q:
|
||||
return Q(**{self._lookup('lt'): other})
|
||||
|
||||
def __le__(self, other):
|
||||
def __le__(self, other) -> Q:
|
||||
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})
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f'JsonKeyExpr({self.field_name}[{self.key!r}])'
|
||||
|
||||
|
||||
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.key = key
|
||||
self.type_hint = type_hint
|
||||
|
||||
def _lookup(self, suffix=''):
|
||||
def _lookup(self, suffix: str = '') -> str:
|
||||
if suffix:
|
||||
return f'{self.field_name}__{self.key}__{suffix}'
|
||||
return f'{self.field_name}__{self.key}'
|
||||
|
||||
def between(self, low, high):
|
||||
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> Q: # type: ignore[override]
|
||||
return Q(**{self._lookup(): other})
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other) -> Q: # type: ignore[override]
|
||||
return ~Q(**{self._lookup(): other})
|
||||
|
||||
def __gt__(self, other):
|
||||
def __gt__(self, other) -> Q:
|
||||
return Q(**{self._lookup('gt'): other})
|
||||
|
||||
def __ge__(self, other):
|
||||
def __ge__(self, other) -> Q:
|
||||
return Q(**{self._lookup('gte'): other})
|
||||
|
||||
def __lt__(self, other):
|
||||
def __lt__(self, other) -> Q:
|
||||
return Q(**{self._lookup('lt'): other})
|
||||
|
||||
def __le__(self, other):
|
||||
def __le__(self, other) -> Q:
|
||||
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))
|
||||
return f'CastJsonExpr({self.field_name}[{self.key!r}]->{type_name})'
|
||||
|
||||
|
||||
class ForeignKeyIdDescriptor:
|
||||
"""FK 字段描述符:类级别访问返回 FieldExpression,实例级别访问/设置委托给原始 Django 字段。"""
|
||||
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
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# QModel 元类与基类
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class QModelBase(models.base.ModelBase):
|
||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||
# 不再强制设置 db_column —— Django 会自动处理
|
||||
"""QModel 元类,负责:
|
||||
|
||||
1. 根据 ``GVSDSDK_MANAGE_TABLES`` 设置动态控制 ``managed``
|
||||
2. 包装字段描述符,使类级别访问返回 FieldExpression
|
||||
"""
|
||||
|
||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||
# 动态设置 managed:子服务器可设 GVSDSDK_MANAGE_TABLES=True 让 gvsdsdk 管理表
|
||||
meta = namespace.get('Meta')
|
||||
if meta and getattr(meta, 'app_label', None) == 'gvsdsdk':
|
||||
@@ -378,12 +423,12 @@ def _wrap_field_descriptors(cls: Type) -> None:
|
||||
实例级别完全委托给 Django 原生描述符。
|
||||
|
||||
Django 的字段描述符(DeferredAttribute / ForwardManyToOneDescriptor):
|
||||
- __get__(None, cls) → 返回字段对象本身(类级别)
|
||||
- __get__(instance, cls) → 返回字段值(实例级别)
|
||||
- ``__get__(None, cls)`` → 返回字段对象本身(类级别)
|
||||
- ``__get__(instance, cls)`` → 返回字段值(实例级别)
|
||||
|
||||
我们包装后:
|
||||
- __get__(None, cls) → 返回 FieldExpression(类级别,用于链式 OOP)
|
||||
- __get__(instance, cls) → 委托给原生描述符(实例级别,正常行为)
|
||||
- ``__get__(None, cls)`` → 返回 FieldExpression(类级别,用于链式 OOP)
|
||||
- ``__get__(instance, cls)`` → 委托给原生描述符(实例级别,正常行为)
|
||||
"""
|
||||
from django.db.models.query_utils import DeferredAttribute
|
||||
from django.db.models.fields.related_descriptors import (
|
||||
@@ -408,13 +453,18 @@ def _wrap_field_descriptors(cls: Type) -> None:
|
||||
|
||||
|
||||
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._model = model_class
|
||||
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):
|
||||
if obj is None:
|
||||
@@ -423,7 +473,7 @@ class _FieldExpressionWrapper:
|
||||
# 实例级别访问:委托给原生 Django 描述符
|
||||
return self._original.__get__(obj, objtype)
|
||||
|
||||
def __set__(self, obj, value):
|
||||
def __set__(self, obj, value) -> None:
|
||||
if self._has_set:
|
||||
# FK/O2O 描述符有 __set__
|
||||
self._original.__set__(obj, value)
|
||||
@@ -431,19 +481,24 @@ class _FieldExpressionWrapper:
|
||||
# 普通字段(DeferredAttribute)没有 __set__,直接写 __dict__
|
||||
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):
|
||||
"""QModel 兼容层基类,提供 SQLAlchemy 风格的链式 OOP 查询接口。
|
||||
|
||||
所有继承 QModel 的模型自动获得:
|
||||
|
||||
- ``Model.query`` : 返回 FluentQuery,支持链式查询
|
||||
- ``Model.field_name`` : 类级别访问返回 FieldExpression,支持表达式查询
|
||||
- ``Model.field_name`` : 实例级别访问返回字段值(与普通 Django Model 一致)
|
||||
- ``Model[condition]`` : 快捷 filter 语法
|
||||
|
||||
用法示例::
|
||||
|
||||
# 链式查询
|
||||
orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list()
|
||||
orders = Dingdan.query.filter(zhuangtai=8).order_by('-CreateTime').to_list()
|
||||
|
||||
# 字段表达式
|
||||
Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3]))
|
||||
@@ -456,28 +511,30 @@ class QModel(models.Model, metaclass=QModelBase):
|
||||
|
||||
# 分页
|
||||
Dingdan.query.paginate(page=1, per_page=20)
|
||||
|
||||
# 快捷 filter
|
||||
Dingdan[Dingdan.zhuangtai == 8].order_by('-CreateTime').to_list()
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classproperty
|
||||
def query(cls) -> FluentQuery:
|
||||
def query(cls) -> FluentQuery[Self]:
|
||||
"""返回 FluentQuery 兼容层查询构建器,支持链式 OOP 查询。"""
|
||||
return FluentQuery(cls)
|
||||
|
||||
@classmethod
|
||||
def __class_getitem__(cls, item) -> FluentQuery:
|
||||
"""支持 Model[condition] 语法,返回 filter 后的 FluentQuery。
|
||||
def __class_getitem__(cls, item) -> FluentQuery[Self]:
|
||||
"""支持 ``Model[condition]`` 语法,返回 filter 后的 FluentQuery。
|
||||
|
||||
用法::
|
||||
|
||||
Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list()
|
||||
Dingdan[Dingdan.zhuangtai == 8].order_by('-CreateTime').to_list()
|
||||
Dingdan[{'zhuangtai': 8}].to_list()
|
||||
"""
|
||||
if isinstance(item, Q):
|
||||
return cls.query.filter(item)
|
||||
if isinstance(item, dict):
|
||||
return cls.query.filter(**item)
|
||||
return cls.query.filter(item)
|
||||
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
4. 鉴权规则层:操作权限校验(角色汇总)+ 数据权限校验(组织属性)
|
||||
|
||||
使用方式(子服务器):
|
||||
from gvsdsdk.models import User, Permission, Role, UserRole, RolePermission
|
||||
from gvsdsdk.models import Permission, Role, UserRole, RolePermission
|
||||
|
||||
# 查询所有启用的用户
|
||||
users = User.objects.using('saas').filter(UserAccountLicense=1)
|
||||
# 查询所有启用的用户(需在业务子类中定义具体 User 模型)
|
||||
users = User.objects.filter(UserAccountLicense=1)
|
||||
|
||||
# 或配置 DatabaseRouter 后直接使用
|
||||
users = User.objects.filter(UserAccountLicense=1)
|
||||
@@ -27,6 +27,10 @@
|
||||
3. PascalCase:字段名、方法名遵循帕斯卡命名法
|
||||
4. 兼容:db_table、db_column 与 SAAS 主库完全一致
|
||||
5. 租户隔离:所有权限相关数据依附于独立租户空间,不同租户互相不可见
|
||||
|
||||
注意:User 为抽象基类,仅包含 SAAS 主库标准字段。
|
||||
业务子类应继承 User 并在单独文件中添加业务字段和方法。
|
||||
参见 gvsdsdk/business_models.py。
|
||||
"""
|
||||
|
||||
import uuid as _uuid
|
||||
@@ -172,9 +176,14 @@ class UserManager(BaseUserManager):
|
||||
|
||||
# === 用户表 ===
|
||||
class User(QModel):
|
||||
"""用户表——系统核心身份模型(映射 SAAS 主库 User 表)"""
|
||||
"""用户表——系统核心身份模型(抽象基类,映射 SAAS 主库 User 表标准字段)
|
||||
|
||||
UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID')
|
||||
本类为抽象基类,仅包含 SAAS 主库 User 表的标准字段和方法。
|
||||
业务子类应继承本类,在单独文件中添加业务字段和方法。
|
||||
参见 gvsdsdk/business_models.py。
|
||||
"""
|
||||
|
||||
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='用户名')
|
||||
UserPassword = models.CharField(max_length=60, default='', db_column='UserPassword', verbose_name='密码')
|
||||
UserPositionStatus = models.SmallIntegerField(default=0, db_column='UserPositionStatus', verbose_name='职位状态')
|
||||
@@ -186,17 +195,13 @@ class User(QModel):
|
||||
UserTele = models.CharField(max_length=16, null=True, blank=True, db_column='UserTele', verbose_name='电话')
|
||||
UserCreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UserCreateTime', verbose_name='创建时间')
|
||||
UserLastLoginDate = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UserLastLoginDate', verbose_name='最后登录')
|
||||
IsStaff = models.BooleanField(default=False, db_column='IsStaff', verbose_name='是否员工')
|
||||
IsSuperuser = models.BooleanField(default=False, db_column='IsSuperuser', verbose_name='是否超级用户')
|
||||
|
||||
objects = UserManager()
|
||||
# Django auth 框架必需(由具体子类覆盖)
|
||||
USERNAME_FIELD = 'UserName'
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'User'
|
||||
verbose_name = '用户'
|
||||
verbose_name_plural = '用户'
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return self.UserName
|
||||
@@ -240,15 +245,21 @@ class User(QModel):
|
||||
self.UserPassword = hashed.decode('utf-8')
|
||||
|
||||
def CheckPassword(self, RawPassword):
|
||||
"""验证密码"""
|
||||
"""验证密码——兼容 bcrypt 哈希和明文存储"""
|
||||
import bcrypt
|
||||
stored = self.UserPassword or ''
|
||||
# bcrypt 哈希以 $2b$ 或 $2a$ 开头,长度固定 60
|
||||
if stored.startswith(('$2b$', '$2a$')) and len(stored) == 60:
|
||||
try:
|
||||
return bcrypt.checkpw(
|
||||
RawPassword.encode('utf-8'),
|
||||
self.UserPassword.encode('utf-8')
|
||||
stored.encode('utf-8')
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
# 明文密码(旧数据兼容)
|
||||
return stored == RawPassword
|
||||
|
||||
def GetTele(self):
|
||||
"""获取电话,空则返回默认文本"""
|
||||
@@ -291,7 +302,7 @@ class User(QModel):
|
||||
PermStatus=1,
|
||||
).exists()
|
||||
|
||||
# Django auth 框架兼容(内部使用蛇形命名)
|
||||
# ── Django auth 框架必需属性 ──
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.IsActive
|
||||
@@ -330,7 +341,7 @@ class UserAvatar(QModel):
|
||||
AvatarFileType = models.CharField(max_length=16, default='png', db_column='AvatarFileType', verbose_name='文件类型')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'UserAvatar'
|
||||
verbose_name = '用户头像'
|
||||
@@ -348,10 +359,10 @@ class User2FA(QModel):
|
||||
IsEnabled = models.SmallIntegerField(default=0, db_column='IsEnabled', verbose_name='是否启用')
|
||||
Verified = models.SmallIntegerField(default=0, db_column='Verified', verbose_name='已验证')
|
||||
LastUsedAt = models.DateTimeField(null=True, blank=True, db_column='LastUsedAt', verbose_name='最后使用')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'User2FA'
|
||||
verbose_name = '双因素认证'
|
||||
@@ -369,7 +380,7 @@ class UserPending2FA(QModel):
|
||||
ExpireAt = models.DateTimeField(db_column='ExpireAt', verbose_name='过期时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'UserPending2FA'
|
||||
verbose_name = '2FA待确认'
|
||||
@@ -396,7 +407,7 @@ class EphemeralToken(QModel):
|
||||
ELTStatus = models.SmallIntegerField(default=1, db_index=True, db_column='ELTStatus', verbose_name='状态')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'EphemeralToken'
|
||||
verbose_name = '临时令牌'
|
||||
@@ -425,7 +436,7 @@ class UserLoginLog(QModel):
|
||||
UserAgent = models.CharField(max_length=512, default='', db_column='UserAgent', verbose_name='用户代理')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'UserLoginLog'
|
||||
verbose_name = '登录日志'
|
||||
@@ -439,10 +450,10 @@ class AccountActivation(QModel):
|
||||
|
||||
AccountActivationCode = FixedBinaryField(primary_key=True, binary_length=6, db_column='AccountActivationCode', verbose_name='激活码')
|
||||
UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'AccountActivation'
|
||||
verbose_name = '账户激活'
|
||||
@@ -469,10 +480,14 @@ class Role(QModel):
|
||||
AssignScope = models.SmallIntegerField(default=0, db_column='AssignScope', verbose_name='分配可见范围')
|
||||
AssignScopeUUID = UUIDField(null=True, blank=True, db_column='AssignScopeUUID', verbose_name='分配范围UUID')
|
||||
RoleDesc = models.CharField(max_length=512, default='', db_column='RoleDesc', verbose_name='角色描述')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
ClubID = models.CharField(
|
||||
max_length=16, default='', blank=True, db_column='club_id',
|
||||
db_index=True, verbose_name='所属俱乐部(空=集团全局角色)',
|
||||
)
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'Role'
|
||||
verbose_name = '角色'
|
||||
@@ -490,10 +505,12 @@ class Role(QModel):
|
||||
|
||||
def GetUsers(self):
|
||||
"""获取拥有该角色的用户列表"""
|
||||
from django.apps import apps
|
||||
UserModel = apps.get_model('gvsdsdk', 'User')
|
||||
user_uuids = UserRole.objects.filter(
|
||||
RoleUUID=self.RoleUUID
|
||||
).values_list('UserUUID', flat=True)
|
||||
return User.objects.filter(UserUUID__in=user_uuids)
|
||||
return UserModel.objects.filter(UserUUID__in=user_uuids)
|
||||
|
||||
|
||||
# === 权限表 ===
|
||||
@@ -513,10 +530,10 @@ class Permission(QModel):
|
||||
PermCategory = models.CharField(max_length=64, default='', db_column='PermCategory', verbose_name='权限分类')
|
||||
PermStatus = models.SmallIntegerField(default=1, db_column='PermStatus', verbose_name='是否启用')
|
||||
PermDesc = models.CharField(max_length=512, default='', db_column='PermDesc', verbose_name='描述')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'Permission'
|
||||
verbose_name = '权限'
|
||||
@@ -552,10 +569,10 @@ class UserRole(QModel):
|
||||
# 原 gvsds 中为 ForeignKey('Role'),此处改为 UUIDField
|
||||
RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID')
|
||||
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'UserRole'
|
||||
verbose_name = '用户角色关联'
|
||||
@@ -574,10 +591,10 @@ class RolePermission(QModel):
|
||||
RolePermUUID = UUIDField(primary_key=True, db_column='RolePermUUID', verbose_name='关联UUID')
|
||||
RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID')
|
||||
PermUUID = UUIDField(db_column='PermUUID', verbose_name='权限UUID')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'RolePermission'
|
||||
verbose_name = '角色权限关联'
|
||||
@@ -594,7 +611,7 @@ class UserABAC(QModel):
|
||||
HierarchicalPermissionIdentifiers = models.CharField(max_length=256, db_column='HierarchicalPermissionIdentifiers', verbose_name='层级权限标识')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'UserABAC'
|
||||
verbose_name = '用户ABAC'
|
||||
@@ -610,7 +627,7 @@ class Password(QModel):
|
||||
Password = models.CharField(max_length=1024, db_column='Password', verbose_name='密码哈希')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'Password'
|
||||
verbose_name = '密码'
|
||||
@@ -635,11 +652,11 @@ class Shop(QModel):
|
||||
ShopContact = models.CharField(max_length=128, default='', db_column='ShopContact', verbose_name='联系人')
|
||||
ShopPhone = models.CharField(max_length=32, default='', db_column='ShopPhone', verbose_name='联系电话')
|
||||
ShopDesc = models.CharField(max_length=512, default='', db_column='ShopDesc', verbose_name='描述')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'Shop'
|
||||
verbose_name = '商铺'
|
||||
@@ -668,10 +685,10 @@ class RoleShop(QModel):
|
||||
RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID')
|
||||
ShopUUID = UUIDField(db_column='ShopUUID', verbose_name='商铺UUID')
|
||||
IsMainShop = models.SmallIntegerField(default=0, db_column='IsMainShop', verbose_name='主商铺')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'RoleShop'
|
||||
verbose_name = '角色商铺关联'
|
||||
@@ -696,11 +713,11 @@ class PositionTemplate(QModel):
|
||||
TemplateCode = models.CharField(max_length=64, default='', db_column='TemplateCode', verbose_name='模板编码')
|
||||
TemplateStatus = models.SmallIntegerField(default=1, db_column='TemplateStatus', verbose_name='模板状态')
|
||||
TemplateDesc = models.CharField(max_length=512, default='', db_column='TemplateDesc', verbose_name='模板描述')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'PositionTemplate'
|
||||
verbose_name = '岗位模板'
|
||||
@@ -747,10 +764,10 @@ class PositionTemplatePermission(QModel):
|
||||
PTPermUUID = UUIDField(primary_key=True, db_column='PTPermUUID', verbose_name='关联UUID')
|
||||
PositionTemplateUUID = UUIDField(db_column='PositionTemplateUUID', verbose_name='岗位模板UUID')
|
||||
PermUUID = UUIDField(db_column='PermUUID', verbose_name='权限UUID')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'PositionTemplatePermission'
|
||||
verbose_name = '岗位模板权限关联'
|
||||
@@ -774,11 +791,11 @@ class Position(QModel):
|
||||
PositionLevel = models.SmallIntegerField(default=0, db_column='PositionLevel', verbose_name='职务级别')
|
||||
PositionStatus = models.SmallIntegerField(default=1, db_column='PositionStatus', verbose_name='状态')
|
||||
PositionDesc = models.CharField(max_length=512, default='', db_column='PositionDesc', verbose_name='描述')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'Position'
|
||||
verbose_name = '人事职务标签'
|
||||
@@ -803,10 +820,10 @@ class UserDept(QModel):
|
||||
DeptUUID = UUIDField(db_column='DeptUUID', verbose_name='部门UUID')
|
||||
PositionUUID = UUIDField(null=True, blank=True, db_column='PositionUUID', verbose_name='职务标签UUID')
|
||||
IsMainDept = models.SmallIntegerField(default=1, db_column='IsMainDept', verbose_name='是否主部门')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
managed = True
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'UserDept'
|
||||
verbose_name = '员工部门关联'
|
||||
|
||||
532
gvsdsdk/msync/orm_handler.py
Normal file
532
gvsdsdk/msync/orm_handler.py
Normal file
@@ -0,0 +1,532 @@
|
||||
"""MSYNC ORM 中转处理器
|
||||
|
||||
子服务器端注册 Socket.IO 事件处理器,接收主服务器发来的 ORM 查询请求,
|
||||
在本地数据库执行 SQL 并返回结果。
|
||||
|
||||
用法(子服务器端):
|
||||
from gvsdsdk.msync.orm_handler import ORMRelayHandler
|
||||
|
||||
handler = ORMRelayHandler()
|
||||
handler.Register(sio_client) # 注册到 MSyncSocketClient 实例
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ORMRelayHandler:
|
||||
"""ORM 中转处理器——在子服务器端执行本地 SQL 查询
|
||||
|
||||
通过 MSYNC Socket.IO 事件协议,主服务器将 ORM 请求转发给子服务器,
|
||||
子服务器在本地数据库执行查询后返回结果。
|
||||
"""
|
||||
|
||||
def Register(self, sio_client):
|
||||
"""将 ORM 事件处理器注册到 MSyncSocketClient 实例
|
||||
|
||||
Args:
|
||||
sio_client: MSyncSocketClient 实例,需要有 _sio 属性
|
||||
"""
|
||||
sio = sio_client._sio
|
||||
|
||||
@sio.on('msync:orm:tables', namespace='/msync')
|
||||
def on_orm_tables(data):
|
||||
return self.HandleTables(data)
|
||||
|
||||
@sio.on('msync:orm:columns', namespace='/msync')
|
||||
def on_orm_columns(data):
|
||||
return self.HandleColumns(data)
|
||||
|
||||
@sio.on('msync:orm:list', namespace='/msync')
|
||||
def on_orm_list(data):
|
||||
return self.HandleList(data)
|
||||
|
||||
@sio.on('msync:orm:add', namespace='/msync')
|
||||
def on_orm_add(data):
|
||||
return self.HandleAdd(data)
|
||||
|
||||
@sio.on('msync:orm:modify', namespace='/msync')
|
||||
def on_orm_modify(data):
|
||||
return self.HandleModify(data)
|
||||
|
||||
@sio.on('msync:orm:delete', namespace='/msync')
|
||||
def on_orm_delete(data):
|
||||
return self.HandleDelete(data)
|
||||
|
||||
logger.info('[ORM] ORM 中转处理器已注册')
|
||||
|
||||
def _GetConnection(self):
|
||||
"""获取本地数据库连接(Django default)"""
|
||||
from django.db import connections
|
||||
conn = connections['default']
|
||||
conn.ensure_connection()
|
||||
return conn
|
||||
|
||||
# ─── 事件处理 ──────────────────────────────────────────────
|
||||
|
||||
def HandleTables(self, data):
|
||||
"""列出本地数据库中所有用户表"""
|
||||
try:
|
||||
conn = self._GetConnection()
|
||||
except Exception as e:
|
||||
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
tables = self._ListTables(cursor)
|
||||
return {'code': 0, 'msg': 'success', 'data': tables}
|
||||
except Exception as e:
|
||||
logger.error(f'[ORM] HandleTables 异常: {e}')
|
||||
return {'code': 1, 'msg': f'查询失败: {e}'}
|
||||
|
||||
def HandleColumns(self, data):
|
||||
"""列出指定表的列信息"""
|
||||
table_name = (data or {}).get('Table', '')
|
||||
if not table_name:
|
||||
return {'code': 1, 'msg': '缺少 Table 参数'}
|
||||
|
||||
try:
|
||||
conn = self._GetConnection()
|
||||
except Exception as e:
|
||||
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
columns = self._ListColumns(cursor, table_name)
|
||||
return {'code': 0, 'msg': 'success', 'data': columns}
|
||||
except Exception as e:
|
||||
logger.error(f'[ORM] HandleColumns 异常: {e}')
|
||||
return {'code': 1, 'msg': f'查询失败: {e}'}
|
||||
|
||||
def HandleList(self, data):
|
||||
"""分页查询表数据"""
|
||||
table_name = (data or {}).get('Table', '')
|
||||
if not table_name:
|
||||
return {'code': 1, 'msg': '缺少 Table 参数'}
|
||||
|
||||
try:
|
||||
conn = self._GetConnection()
|
||||
except Exception as e:
|
||||
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
result = self._QueryTable(cursor, table_name, data or {})
|
||||
if result.get('error'):
|
||||
return {'code': 1, 'msg': result['error'], 'data': None}
|
||||
return {'code': 0, 'msg': 'success', 'data': result}
|
||||
except Exception as e:
|
||||
logger.error(f'[ORM] HandleList 异常: {e}')
|
||||
return {'code': 1, 'msg': f'查询失败: {e}'}
|
||||
|
||||
def HandleAdd(self, data):
|
||||
"""新增记录"""
|
||||
table_name = (data or {}).get('Table', '')
|
||||
record = (data or {}).get('Record', {})
|
||||
if not table_name:
|
||||
return {'code': 1, 'msg': '缺少 Table 参数'}
|
||||
|
||||
try:
|
||||
conn = self._GetConnection()
|
||||
except Exception as e:
|
||||
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
result = self._AddRecord(cursor, table_name, record)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f'[ORM] HandleAdd 异常: {e}')
|
||||
return {'code': 1, 'msg': f'新增失败: {e}'}
|
||||
|
||||
def HandleModify(self, data):
|
||||
"""修改记录"""
|
||||
table_name = (data or {}).get('Table', '')
|
||||
record = (data or {}).get('Record', {})
|
||||
if not table_name:
|
||||
return {'code': 1, 'msg': '缺少 Table 参数'}
|
||||
|
||||
try:
|
||||
conn = self._GetConnection()
|
||||
except Exception as e:
|
||||
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
result = self._ModifyRecord(cursor, table_name, record)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f'[ORM] HandleModify 异常: {e}')
|
||||
return {'code': 1, 'msg': f'修改失败: {e}'}
|
||||
|
||||
def HandleDelete(self, data):
|
||||
"""删除记录"""
|
||||
table_name = (data or {}).get('Table', '')
|
||||
record = (data or {}).get('Record', {})
|
||||
if not table_name:
|
||||
return {'code': 1, 'msg': '缺少 Table 参数'}
|
||||
|
||||
try:
|
||||
conn = self._GetConnection()
|
||||
except Exception as e:
|
||||
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
result = self._DeleteRecord(cursor, table_name, record)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f'[ORM] HandleDelete 异常: {e}')
|
||||
return {'code': 1, 'msg': f'删除失败: {e}'}
|
||||
|
||||
# ─── SQL 执行 ──────────────────────────────────────────────
|
||||
|
||||
def _ListTables(self, cursor):
|
||||
"""列出所有用户表,含 i18n 标题"""
|
||||
cursor.execute(
|
||||
"SELECT TABLE_NAME, TABLE_ROWS, TABLE_COMMENT FROM information_schema.TABLES "
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' "
|
||||
"ORDER BY TABLE_NAME"
|
||||
)
|
||||
|
||||
from django.apps import apps
|
||||
# 构建表名→中文标题映射
|
||||
model_verbose = {}
|
||||
for model in apps.get_models():
|
||||
meta = model._meta
|
||||
# 优先使用 table_title(自定义 Meta 属性),其次 verbose_name
|
||||
table_title = getattr(meta, 'table_title', None)
|
||||
title = str(table_title) if table_title else (str(meta.verbose_name) if meta.verbose_name else '')
|
||||
if title:
|
||||
model_verbose[meta.object_name] = title
|
||||
model_verbose[meta.db_table] = title
|
||||
|
||||
result = []
|
||||
for row in cursor.fetchall():
|
||||
table_name, table_rows, table_comment = row
|
||||
# 优先级:TABLE_COMMENT > table_title/verbose_name > 表名
|
||||
title = table_comment or model_verbose.get(table_name) or table_name
|
||||
result.append({'name': table_name, 'title': title, 'count': table_rows or 0})
|
||||
return result
|
||||
|
||||
def _ListColumns(self, cursor, table_name):
|
||||
"""列出指定表的列信息"""
|
||||
cursor.execute(
|
||||
"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY, EXTRA, COLUMN_TYPE, COLUMN_COMMENT "
|
||||
"FROM information_schema.COLUMNS "
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s "
|
||||
"ORDER BY ORDINAL_POSITION",
|
||||
[table_name]
|
||||
)
|
||||
|
||||
model_verbose = self._FindModelVerboseNames(table_name)
|
||||
logger.info(f'[ORM] _ListColumns table={table_name} model_verbose={model_verbose}')
|
||||
|
||||
columns = []
|
||||
for row in cursor.fetchall():
|
||||
col_name, data_type, is_nullable, col_key, extra, col_type, col_comment = row
|
||||
is_pk = col_key == 'PRI'
|
||||
is_auto = 'auto_increment' in (extra or '').lower()
|
||||
|
||||
if data_type in ('int', 'bigint', 'tinyint', 'smallint', 'mediumint'):
|
||||
ftype = 'number'
|
||||
elif data_type in ('float', 'double', 'decimal'):
|
||||
ftype = 'number'
|
||||
elif data_type in ('datetime', 'timestamp'):
|
||||
ftype = 'datetime'
|
||||
elif data_type in ('date',):
|
||||
ftype = 'date'
|
||||
elif data_type in ('time',):
|
||||
ftype = 'time'
|
||||
elif data_type in ('json',):
|
||||
ftype = 'textarea'
|
||||
elif data_type in ('text', 'mediumtext', 'longtext', 'tinytext'):
|
||||
ftype = 'textarea'
|
||||
elif data_type in ('binary', 'varbinary', 'blob'):
|
||||
ftype = 'uuid'
|
||||
else:
|
||||
ftype = 'text'
|
||||
|
||||
title = col_comment or model_verbose.get(col_name) or col_name
|
||||
|
||||
columns.append({
|
||||
'field': col_name,
|
||||
'title': title,
|
||||
'type': ftype,
|
||||
'editable': not is_pk and not is_auto,
|
||||
'sortable': True,
|
||||
'filterable': True,
|
||||
'primaryKey': is_pk,
|
||||
'hideInForm': is_pk,
|
||||
'required': is_nullable == 'NO' and not is_auto,
|
||||
'dbType': col_type,
|
||||
})
|
||||
return columns
|
||||
|
||||
def _QueryTable(self, cursor, table_name, params):
|
||||
"""分页查询表数据"""
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM information_schema.TABLES "
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s",
|
||||
[table_name]
|
||||
)
|
||||
if cursor.fetchone()[0] == 0:
|
||||
return {'records': [], 'total': 0, 'error': f'表不存在: {table_name}'}
|
||||
|
||||
col_info = self._GetTableColumns(cursor, table_name)
|
||||
pk_cols = {k: v for k, v in col_info.items() if v['is_pk']}
|
||||
|
||||
where_parts = []
|
||||
where_values = []
|
||||
for pk_name in pk_cols:
|
||||
pk_val = params.get(pk_name, '')
|
||||
if pk_val:
|
||||
where_parts.append(f'`{pk_name}` = %s')
|
||||
where_values.append(self._ConvertValueForDB(pk_val, pk_cols[pk_name]))
|
||||
|
||||
keyword = params.get('keyword', '')
|
||||
if not where_parts and keyword:
|
||||
cursor.execute(
|
||||
"SELECT COLUMN_NAME FROM information_schema.COLUMNS "
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s "
|
||||
"AND DATA_TYPE IN ('varchar', 'char', 'text', 'mediumtext', 'longtext')",
|
||||
[table_name]
|
||||
)
|
||||
text_cols = [row[0] for row in cursor.fetchall()]
|
||||
if text_cols:
|
||||
conditions = [f'`{c}` LIKE %s' for c in text_cols]
|
||||
or_clause = ' OR '.join(conditions)
|
||||
where_parts.append(f'({or_clause})')
|
||||
where_values.extend([f'%{keyword}%'] * len(text_cols))
|
||||
|
||||
where_clause = (' WHERE ' + ' AND '.join(where_parts)) if where_parts else ''
|
||||
|
||||
if where_clause:
|
||||
cursor.execute(f'SELECT COUNT(*) FROM `{table_name}`{where_clause}', where_values)
|
||||
total = cursor.fetchone()[0]
|
||||
else:
|
||||
cursor.execute(f'SELECT COUNT(*) FROM `{table_name}`')
|
||||
total = cursor.fetchone()[0]
|
||||
|
||||
try:
|
||||
page = max(int(params.get('PageIndex', 1)), 1)
|
||||
except (ValueError, TypeError):
|
||||
page = 1
|
||||
try:
|
||||
page_size = min(max(int(params.get('PageSize', 20)), 1), 200)
|
||||
except (ValueError, TypeError):
|
||||
page_size = 20
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
cursor.execute(
|
||||
f'SELECT * FROM `{table_name}`{where_clause} ORDER BY 1 LIMIT %s OFFSET %s',
|
||||
where_values + [page_size, offset]
|
||||
)
|
||||
desc = [d[0] for d in cursor.description]
|
||||
records = []
|
||||
for row in cursor.fetchall():
|
||||
record = {}
|
||||
for i, val in enumerate(row):
|
||||
val = self._SerializeValue(val)
|
||||
record[desc[i]] = val
|
||||
records.append(record)
|
||||
|
||||
return {'records': records, 'total': total, 'page': page, 'PageSize': page_size}
|
||||
|
||||
def _AddRecord(self, cursor, table_name, record):
|
||||
"""新增记录"""
|
||||
if not self._ValidateTableName(cursor, table_name):
|
||||
return {'code': 1, 'msg': f'表 {table_name} 不存在'}
|
||||
|
||||
col_info = self._GetTableColumns(cursor, table_name)
|
||||
|
||||
fields = []
|
||||
values = []
|
||||
for key, val in record.items():
|
||||
if key not in col_info:
|
||||
continue
|
||||
if col_info[key]['is_auto']:
|
||||
continue
|
||||
fields.append(f'`{key}`')
|
||||
values.append(self._ConvertValueForDB(val, col_info[key]))
|
||||
|
||||
if not fields:
|
||||
return {'code': 1, 'msg': '没有可插入的字段'}
|
||||
|
||||
placeholders = ', '.join(['%s'] * len(fields))
|
||||
sql = f'INSERT INTO `{table_name}` ({", ".join(fields)}) VALUES ({placeholders})'
|
||||
cursor.execute(sql, values)
|
||||
return {'code': 0, 'msg': '新增成功'}
|
||||
|
||||
def _ModifyRecord(self, cursor, table_name, record):
|
||||
"""修改记录"""
|
||||
if not self._ValidateTableName(cursor, table_name):
|
||||
return {'code': 1, 'msg': f'表 {table_name} 不存在'}
|
||||
|
||||
col_info = self._GetTableColumns(cursor, table_name)
|
||||
pk_cols = {k: v for k, v in col_info.items() if v['is_pk']}
|
||||
|
||||
if not pk_cols:
|
||||
return {'code': 1, 'msg': '表没有主键,无法定位记录'}
|
||||
|
||||
set_parts = []
|
||||
set_values = []
|
||||
for key, val in record.items():
|
||||
if key not in col_info or key in pk_cols:
|
||||
continue
|
||||
set_parts.append(f'`{key}` = %s')
|
||||
set_values.append(self._ConvertValueForDB(val, col_info[key]))
|
||||
|
||||
if not set_parts:
|
||||
return {'code': 0, 'msg': '没有需要更新的字段'}
|
||||
|
||||
where_parts = []
|
||||
where_values = []
|
||||
for pk_name in pk_cols:
|
||||
if pk_name not in record:
|
||||
return {'code': 1, 'msg': f'缺少主键字段: {pk_name}'}
|
||||
where_parts.append(f'`{pk_name}` = %s')
|
||||
where_values.append(self._ConvertValueForDB(record[pk_name], pk_cols[pk_name]))
|
||||
|
||||
sql = f'UPDATE `{table_name}` SET {", ".join(set_parts)} WHERE {" AND ".join(where_parts)}'
|
||||
cursor.execute(sql, set_values + where_values)
|
||||
|
||||
if cursor.rowcount == 0:
|
||||
return {'code': 1, 'msg': '记录不存在或未变更'}
|
||||
return {'code': 0, 'msg': '修改成功'}
|
||||
|
||||
def _DeleteRecord(self, cursor, table_name, record):
|
||||
"""删除记录"""
|
||||
if not self._ValidateTableName(cursor, table_name):
|
||||
return {'code': 1, 'msg': f'表 {table_name} 不存在'}
|
||||
|
||||
col_info = self._GetTableColumns(cursor, table_name)
|
||||
pk_cols = {k: v for k, v in col_info.items() if v['is_pk']}
|
||||
|
||||
if not pk_cols:
|
||||
return {'code': 1, 'msg': '表没有主键,无法定位记录'}
|
||||
|
||||
where_parts = []
|
||||
where_values = []
|
||||
for pk_name in pk_cols:
|
||||
if pk_name not in record:
|
||||
return {'code': 1, 'msg': f'缺少主键字段: {pk_name}'}
|
||||
where_parts.append(f'`{pk_name}` = %s')
|
||||
where_values.append(self._ConvertValueForDB(record[pk_name], pk_cols[pk_name]))
|
||||
|
||||
sql = f'DELETE FROM `{table_name}` WHERE {" AND ".join(where_parts)}'
|
||||
cursor.execute(sql, where_values)
|
||||
|
||||
if cursor.rowcount == 0:
|
||||
return {'code': 1, 'msg': '记录不存在'}
|
||||
return {'code': 0, 'msg': '删除成功'}
|
||||
|
||||
# ─── 工具方法 ──────────────────────────────────────────────
|
||||
|
||||
def _ValidateTableName(self, cursor, table_name):
|
||||
"""验证表名存在"""
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM information_schema.TABLES "
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s",
|
||||
[table_name]
|
||||
)
|
||||
return cursor.fetchone()[0] > 0
|
||||
|
||||
def _GetTableColumns(self, cursor, table_name):
|
||||
"""获取表的列信息"""
|
||||
cursor.execute(
|
||||
"SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, COLUMN_KEY, EXTRA, IS_NULLABLE "
|
||||
"FROM information_schema.COLUMNS "
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s "
|
||||
"ORDER BY ORDINAL_POSITION",
|
||||
[table_name]
|
||||
)
|
||||
cols = {}
|
||||
for row in cursor.fetchall():
|
||||
col_name, data_type, column_type, col_key, extra, is_nullable = row
|
||||
cols[col_name] = {
|
||||
'type': data_type,
|
||||
'column_type': column_type or '',
|
||||
'is_pk': col_key == 'PRI',
|
||||
'is_auto': 'auto_increment' in (extra or '').lower(),
|
||||
'is_nullable': is_nullable == 'YES',
|
||||
}
|
||||
return cols
|
||||
|
||||
def _ConvertValueForDB(self, value, col_info):
|
||||
"""将前端传来的值转换为数据库可接受的类型"""
|
||||
if value is None or value == '':
|
||||
if col_info['is_nullable']:
|
||||
return None
|
||||
return ''
|
||||
|
||||
data_type = col_info['type']
|
||||
|
||||
if data_type in ('int', 'bigint', 'tinyint', 'smallint', 'mediumint'):
|
||||
return int(value)
|
||||
elif data_type in ('float', 'double', 'decimal'):
|
||||
return float(value)
|
||||
elif data_type in ('binary', 'varbinary'):
|
||||
import re
|
||||
hex_str = re.sub(r'[^0-9a-fA-F]', '', str(value))
|
||||
if len(hex_str) == 32:
|
||||
return uuid.UUID(hex_str).bytes
|
||||
return value
|
||||
|
||||
return value
|
||||
|
||||
def _SerializeValue(self, val):
|
||||
"""将数据库值序列化为 JSON 安全格式"""
|
||||
if isinstance(val, bytes):
|
||||
if len(val) == 16:
|
||||
try:
|
||||
return str(uuid.UUID(bytes=val))
|
||||
except ValueError:
|
||||
return val.hex()
|
||||
return val.hex()
|
||||
elif isinstance(val, datetime.datetime):
|
||||
return val.strftime('%Y-%m-%d %H:%M:%S')
|
||||
elif isinstance(val, datetime.date):
|
||||
return val.isoformat()
|
||||
elif isinstance(val, datetime.time):
|
||||
return val.strftime('%H:%M:%S')
|
||||
elif isinstance(val, (bytearray, memoryview)):
|
||||
return bytes(val).hex()
|
||||
return val
|
||||
|
||||
def _FindModelVerboseNames(self, table_name):
|
||||
"""从 Django 模型中获取字段的 verbose_name"""
|
||||
from django.apps import apps
|
||||
verbose_map = {}
|
||||
try:
|
||||
for model in apps.get_models():
|
||||
if model._meta.db_table == table_name:
|
||||
logger.info(f'[ORM] _FindModelVerboseNames matched model={model.__name__} db_table={model._meta.db_table}')
|
||||
for f in model._meta.get_fields():
|
||||
# 获取数据库列名
|
||||
col_name = None
|
||||
if hasattr(f, 'column') and f.column:
|
||||
col_name = f.column
|
||||
elif hasattr(f, 'db_column') and f.db_column:
|
||||
col_name = f.db_column
|
||||
elif hasattr(f, 'attname'):
|
||||
col_name = f.attname
|
||||
|
||||
if not col_name:
|
||||
continue
|
||||
|
||||
vn = getattr(f, 'verbose_name', None)
|
||||
if vn and str(vn) != col_name:
|
||||
verbose_map[col_name] = str(vn)
|
||||
logger.debug(f'[ORM] {col_name} -> {str(vn)}')
|
||||
break
|
||||
else:
|
||||
# 没有匹配的模型
|
||||
available = [m._meta.db_table for m in apps.get_models()]
|
||||
logger.warning(f'[ORM] _FindModelVerboseNames: 未找到 db_table={table_name} 的模型, 可用表: {available[:20]}...')
|
||||
except Exception as e:
|
||||
logger.error(f'[ORM] _FindModelVerboseNames 异常: {e}')
|
||||
return verbose_map
|
||||
0
jituan/__init__.py
Normal file
0
jituan/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user