This commit is contained in:
2026-07-10 14:04:32 +08:00
11 changed files with 631 additions and 126 deletions

View File

View 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):
"""通过小程序用户 IDYonghuPingzheng.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: 源店铺小程序用户 IDYonghuPingzheng.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'],
}

View File

@@ -10,7 +10,7 @@ from .view import (GetRolePermissionView,
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, FaKuanPingTaiShenHeView,
HqbkxxView,BkxgView,CaiwuView,CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView,
@@ -82,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='后台获取客服聊天配置'),

View File

@@ -63,6 +63,7 @@ from .views.system import (
from .views.shops import (
ShopListView, ShopModifyView, ShopPublicTypeAndAuditView, ShopListForProductView,
ShopProductTypeMappingView, ShopProductModifyView, ProductListView, ProductModifyView,
ShopCopyCatalogView,
)
from .views.penalties import (
FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanPingTaiShenHeView,
@@ -117,6 +118,7 @@ __all__ = [
# shops
'ShopListView', 'ShopModifyView', 'ShopPublicTypeAndAuditView', 'ShopListForProductView',
'ShopProductTypeMappingView', 'ShopProductModifyView', 'ProductListView', 'ProductModifyView',
'ShopCopyCatalogView',
# penalties
'FaKuanTongJiView', 'FaKuanLieBiaoView', 'FaKuanChuLiView', 'FaKuanPingTaiShenHeView',
'FaKuanChuangJianView', 'FineApplyView', 'PunishDashouView',

View File

@@ -50,6 +50,7 @@ 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,
@@ -113,6 +114,7 @@ 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]
@@ -125,8 +127,7 @@ class ShopListView(APIView):
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
# 2. 检查是否拥有所需权限999a~999e 任意一个)
required_perms = {'999a', '999b', '999c', '999d', '999e'}
if not required_perms.intersection(set(permissions)):
if not has_perm_code(permissions, *SHOP_MANAGE_PERMS):
return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'})
# 3. 解析请求参数
@@ -220,21 +221,19 @@ class ShopModifyView(APIView):
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
# 转换权限为集合方便判断
user_perms = set(permissions)
action = request.data.get('action', '')
# 2. 根据 action 分派任务
if action == 'update_dianpu':
return self._update_dianpu(request, user_perms)
return self._update_dianpu(request, permissions)
elif action == 'add_dianpu':
return self._add_dianpu(request, user_perms)
return self._add_dianpu(request, permissions)
elif action == 'update_default':
return self._update_default(request, user_perms)
return self._update_default(request, permissions)
else:
return Response({'code': 400, 'msg': '未知操作类型'})
def _update_dianpu(self, request, user_perms):
def _update_dianpu(self, request, permissions):
"""修改店铺信息,精确权限控制"""
dianpu_id = request.data.get('dianpu_id')
if not dianpu_id:
@@ -246,7 +245,7 @@ class ShopModifyView(APIView):
# 基础信息修改 (999e)
if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']):
if '999e' not in user_perms:
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']
@@ -257,7 +256,7 @@ class ShopModifyView(APIView):
# 店铺状态 (999a)
if 'zhuangtai' in request.data:
if '999a' not in user_perms:
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):
@@ -266,13 +265,13 @@ class ShopModifyView(APIView):
# 可提现余额 (999b)
if 'ketixian_yue' in request.data:
if '999b' not in user_perms:
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 '999c' not in user_perms:
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']
@@ -281,7 +280,7 @@ class ShopModifyView(APIView):
# 抽成相关 (999d)
if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data:
if '999d' not in user_perms:
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']
@@ -297,9 +296,9 @@ class ShopModifyView(APIView):
logger.exception("修改店铺失败")
return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
def _add_dianpu(self, request, user_perms):
def _add_dianpu(self, request, permissions):
"""添加店铺,需要权限 999b"""
if '999b' not in user_perms:
if not has_perm_code(permissions, '999b'):
return Response({'code': 403, 'msg': '无权限添加店铺(999b)'})
yonghu_id = request.data.get('yonghu_id')
@@ -346,16 +345,16 @@ class ShopModifyView(APIView):
logger.exception("创建店铺失败")
return Response({'code': 500, 'msg': '创建失败,请稍后重试'})
def _update_default(self, request, user_perms):
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 '999d' not in user_perms:
if modify_commission and not has_perm_code(permissions, '999d'):
return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'})
if modify_withdraw and '999c' not in user_perms:
if modify_withdraw and not has_perm_code(permissions, '999c'):
return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'})
try:
@@ -394,7 +393,7 @@ class ShopPublicTypeAndAuditView(APIView):
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
# 拥有 1199ab/1199abc/1199abd 任一个即可访问
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'})
# ---------- 查询公共商品类型(仅正常审核状态的) ----------
@@ -444,7 +443,7 @@ class ShopListForProductView(APIView):
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问店铺列表'})
# ---------- 安全解析分页参数 ----------
@@ -520,7 +519,7 @@ class ShopProductTypeMappingView(APIView):
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问商品类型映射'})
# ---------- 安全解析参数 ----------
@@ -611,7 +610,7 @@ class ShopProductModifyView(APIView):
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
user_perms = set(permissions)
user_perms = permissions
action = request.data.get('action')
if action == 'update_audit_mode':
@@ -623,7 +622,7 @@ class ShopProductModifyView(APIView):
@transaction.atomic # ✅ 事务修复点
def _update_audit_mode(self, request, user_perms):
if '1199ab' not in user_perms:
if not has_perm_code(permissions, '1199ab'):
return Response({'code': 403, 'msg': '无权限修改审核模式'})
kaiqi = request.data.get('kaiqi_shenhe')
@@ -657,25 +656,25 @@ class ShopProductModifyView(APIView):
# 权限判断
if fengjin is not None:
if fengjin:
if '1199abc' not in user_perms:
if not has_perm_code(user_perms, '1199abc'):
return Response({'code': 403, 'msg': '无权限封禁'})
else:
if '1199abd' not in user_perms:
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 '1199abc' not in user_perms:
if not has_perm_code(user_perms, '1199abc'):
return Response({'code': 403, 'msg': '无权限下架'})
else:
if '1199abd' not in user_perms:
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 '1199abd' not in user_perms:
if not has_perm_code(user_perms, '1199abd'):
return Response({'code': 403, 'msg': '无权限审核通过'})
mapping.shenhe_zhuangtai = bool(shenhe)
@@ -706,7 +705,7 @@ class ProductListView(APIView):
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not set(permissions).intersection(PRODUCT_PERMS):
if not has_perm_code(permissions, *PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无商品管理权限'})
# ----- 安全解析分页与筛选参数 -----
@@ -844,16 +843,15 @@ class ProductModifyView(APIView):
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
user_perms = set(permissions)
action = request.data.get('action', '')
if action != 'update_product':
return Response({'code': 400, 'msg': '未知操作'})
return self._update_product(request, user_perms)
return self._update_product(request, permissions)
@transaction.atomic
def _update_product(self, request, user_perms):
def _update_product(self, request, permissions):
"""执行商品修改,严格校验权限"""
product_id = request.data.get('product_id')
if not product_id:
@@ -877,18 +875,18 @@ class ProductModifyView(APIView):
# ---------- 权限分类检查 ----------
# 1. 封禁/下架操作 (消极操作) -> 1199abc
if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']):
if '1199abc' not in user_perms:
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 '1199abc' not in user_perms:
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 '1199abd' not in user_perms:
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 '1199abd' not in user_perms:
if not has_perm_code(permissions, '1199abd'):
return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'})
# 3. 修改其他数据(非状态字段)-> 1199abe
@@ -896,7 +894,7 @@ class ProductModifyView(APIView):
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 '1199abe' not in user_perms:
if other_fields and not has_perm_code(permissions, '1199abe'):
return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'})
# ---------- 应用修改 ----------
@@ -931,3 +929,45 @@ class ProductModifyView(APIView):
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': '复制失败,请稍后重试'})

View File

@@ -0,0 +1,111 @@
-- ============================================================
-- 店铺相关权限:查询 + 缺失则插入
-- 主权限表Permissiongvsdsdk RBAC角色管理页读取此表
-- 兼容旧表permissionlegacy部分账号仍走 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;

View File

@@ -32,6 +32,17 @@ EXTRA_PERMISSIONS = [
'管理小程序身份装饰标签(与用户管理-身份装饰标签菜单对应)',
'用户管理',
),
# ---------- 店铺管理999 系列:列表/详情/复制/提现等) ----------
('999a', '店铺状态管理', '修改店铺封禁/正常状态', '店铺管理'),
('999b', '店铺余额与创建', '修改可提现余额、添加新店铺', '店铺管理'),
('999c', '店铺提现限额', '修改每日提现限额及开关', '店铺管理'),
('999d', '店铺抽成配置', '修改店铺订单收益抽成及默认抽成费率', '店铺管理'),
('999e', '店铺基本信息', '修改店铺名称、电话、微信号', '店铺管理'),
# ---------- 店铺商品管理1199 系列:商品/类型/审核) ----------
('1199ab', '店铺商品审核模式', '查看/修改店铺商品全局审核开关', '店铺管理'),
('1199abc', '店铺商品封禁下架', '封禁、下架、驳回店铺商品/类型审核', '店铺管理'),
('1199abd', '店铺商品上架解封', '上架、解封、审核通过店铺商品/类型', '店铺管理'),
('1199abe', '店铺商品信息修改', '修改店铺商品标题、价格、库存等基本信息', '店铺管理'),
]

View File

@@ -6,7 +6,7 @@
- formal_cishu=1duoci_fenhong 或 club 默认正式分成。
- formal_cishu>=2仅 duoci_fenhong无配置则不分。
- 体验单:固定 trial_guanshifc / trial_zuzhangfc终身仅可购 1 次。
- 积分:体验 +5正式 +5体验后或直接至 10未买过体验封顶 10
- 积分:首次充值会员(体验/正式均可)加满至 10此后任意会员充值不再加分
- 履约(回调/轮询/余额)均二次校验体验资格;分红失败回滚事务。
财务:每笔成功履约必须 czjilu.zhuangtai=3 + 收支入账record_wechat_income_once
@@ -50,8 +50,6 @@ MEMBER_FAILED_STATUS = 8
MEMBER_CANCELLED_STATUS = 2
ENTITLEMENT_REVOKED_MARKER = '权益已撤销'
MEMBER_JIFEN_CAP = 10
MEMBER_JIFEN_TRIAL_GRANT = 5
MEMBER_JIFEN_FORMAL_GRANT = 5
class MemberRechargeError(Exception):
@@ -145,6 +143,19 @@ def has_formal_member_purchase(yonghuid, huiyuan_id, club_id, exclude_dingdan_id
return False
def has_any_prior_paid_member_recharge(yonghuid, exclude_dingdan_id=None):
"""是否已有成功支付的会员充值单(任意会员类型;不含后台赠送 jine=0"""
qs = Czjilu.query.filter(
yonghuid=yonghuid,
leixing=MEMBER_LEIXING,
zhuangtai=MEMBER_PAID_STATUS,
jine__gt=0,
)
if exclude_dingdan_id:
qs = qs.exclude(dingdan_id=exclude_dingdan_id)
return qs.exists()
def _sync_trial_usage_flags(yonghuid, huiyuan_id, club_id):
"""根据 czjilu 历史回填 huiyuangoumai.has_used_trial。"""
goumai = Huiyuangoumai.query.filter(
@@ -581,7 +592,7 @@ def _apply_member_fenhong(order, huiyuan, formal_cishu=None, trial_guanshi_fc=No
def _apply_member_purchase_jifen(yonghuid, is_trial, huiyuan_id, club_id, current_dingdan_id=None):
"""
会员购积分:体验 +5正式 +5体验后或直接到 10未买过体验封顶 10
会员购积分:首次充值会员(体验/正式/任意类型)加满至 10此后不再加分
"""
try:
dashou = UserDashou.query.get(user__UserUID=yonghuid)
@@ -593,17 +604,14 @@ def _apply_member_purchase_jifen(yonghuid, is_trial, huiyuan_id, club_id, curren
if current >= MEMBER_JIFEN_CAP:
return current
if is_trial:
delta = min(MEMBER_JIFEN_TRIAL_GRANT, MEMBER_JIFEN_CAP - current)
else:
had_trial = has_paid_trial_purchase(
yonghuid, huiyuan_id, club_id, exclude_dingdan_id=current_dingdan_id,
if has_any_prior_paid_member_recharge(yonghuid, exclude_dingdan_id=current_dingdan_id):
logger.info(
'会员购积分跳过(非首次充值) yonghu=%s is_trial=%s huiyuan=%s jifen=%s',
yonghuid, is_trial, huiyuan_id, current,
)
if had_trial:
delta = min(MEMBER_JIFEN_FORMAL_GRANT, MEMBER_JIFEN_CAP - current)
else:
delta = MEMBER_JIFEN_CAP - current
return current
delta = MEMBER_JIFEN_CAP - current
if delta <= 0:
return current

View File

@@ -21,7 +21,6 @@ from django.conf import settings
from django.db import models, transaction, IntegrityError
from django.db.models import F, Sum, Count, Q, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator, EmptyPage
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
@@ -33,7 +32,8 @@ from rest_framework import status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework_simplejwt.tokens import RefreshToken
from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client
from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client, _url_to_oss_key
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
from utils.money import yuan_to_fen
from ..utils import verify_shop_permission
@@ -482,31 +482,38 @@ class QrcodeDownloadView(APIView):
# 2. 获取店铺的二维码相对路径
erweima_relative_url = dianpu.erweima_url
if not erweima_relative_url:
return HttpResponse('店铺暂无二维码', status=404)
return Response({'code': 404, 'msg': '店铺暂无二维码'}, status=404)
# 3. 拼接 OSS 完整访问地址(使用 settings 中的域名)
oss_domain = getattr(settings, 'COS_DOMAIN', '')
if not oss_domain:
logger.error("COS_DOMAIN 未配置")
return HttpResponse('OSS域名未配置', status=500)
oss_key = _url_to_oss_key(erweima_relative_url) or str(erweima_relative_url).lstrip('/')
image_url = oss_domain.rstrip('/') + '/' + erweima_relative_url.lstrip('/')
# 4. 从腾讯云 COS 获取图片内容
# 3. 从腾讯云 COS 获取图片内容
try:
client = get_oss_client()
bucket = getattr(settings, 'COS_BUCKET', '')
response = client.get_object(Bucket=bucket, Key=erweima_relative_url)
image_data = response['Body'].get_raw_stream().read()
except Exception as e:
logger.exception("从COS获取二维码失败")
return HttpResponse('下载失败,请稍后重试', status=502)
cos_resp = client.get_object(Bucket=bucket, Key=oss_key)
image_data = cos_resp['Body'].get_raw_stream().read()
except Exception:
logger.exception("从COS获取二维码失败 key=%s", oss_key)
return Response({'code': 502, 'msg': '下载失败,请稍后重试'}, status=502)
# 5. 构造下载响应,强制浏览器保存为文件
filename = f"店铺二维码_{dianpu.dianpu_mingcheng}.png"
if not image_data:
return Response({'code': 404, 'msg': '二维码文件不存在'}, status=404)
# 店铺二维码正常仅数十 KB超过 2MB 视为 OSS 路径异常
if len(image_data) > 2 * 1024 * 1024:
logger.error(
'店铺二维码文件体积异常 dianpu=%s key=%s size=%s',
dianpu.id, oss_key, len(image_data),
)
return Response({'code': 500, 'msg': '二维码文件异常,请重新生成'}, status=500)
# 4. 返回图片inline 供页面展示attachment 供下载)
filename = f"shop_qrcode_{dianpu.id}.png"
disposition_type = 'inline' if request.GET.get('disposition') == 'inline' else 'attachment'
response = HttpResponse(image_data, content_type='image/png')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
response['Content-Disposition'] = f'{disposition_type}; filename="{filename}"'
response['Content-Length'] = str(len(image_data))
response['Cache-Control'] = 'private, max-age=300'
return response
@@ -649,42 +656,56 @@ class ShopQrcodeView(APIView):
if error_response:
return error_response
# 获取缓存的微信 access_token
access_token = self._get_wx_access_token()
# 获取微信 access_tokenstable_token + 缓存)
access_token = get_weixin_mini_access_token()
if not access_token:
return Response(
{'code': 500, 'msg': '获取微信 access_token 失败'},
{'code': 500, 'msg': '获取微信 access_token 失败,请检查小程序配置'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 构造小程序码请求参数
# 注意scene 最大32个字符我们使用店铺ID
# 构造小程序码请求参数scene 传店铺ID首页解析 scene 进入店铺)
scene = str(dianpu.id)
page_path = "pages/index/index" # 前端小程序首页路径
wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}'
page_path = getattr(settings, 'SHOP_QRCODE_PAGE', 'pages/index/index')
post_data = {
'scene': scene,
'page': page_path,
'width': 430,
'auto_color': False,
'line_color': {'r': 0, 'g': 0, 'b': 0},
'check_path': False,
}
# 调用微信接口生成小程序码
try:
wx_resp = requests.post(wx_url, json=post_data, timeout=10)
except Exception as e:
logger.error(f"请求微信生成二维码接口异常: {e}")
return Response(
{'code': 500, 'msg': '调用微信服务失败'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def _request_wx_qrcode(token):
wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={token}'
return requests.post(wx_url, json=post_data, timeout=15)
# 判断返回是否为图片
if wx_resp.status_code != 200 or 'image' not in wx_resp.headers.get('content-type', ''):
error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {}
errmsg = error_info.get('errmsg', '未知错误')
logger.error(f"微信生成二维码失败: {errmsg}")
wx_resp = _request_wx_qrcode(access_token)
# token 失效时强制刷新重试一次
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_weixin_mini_access_token(force_refresh=True)
if access_token:
wx_resp = _request_wx_qrcode(access_token)
except Exception:
pass
# 判断返回是否为图片(微信错误时可能返回 JSON
content_type = wx_resp.headers.get('content-type', '')
if wx_resp.status_code != 200 or 'image' not in content_type:
errmsg = '未知错误'
try:
error_info = wx_resp.json()
errmsg = error_info.get('errmsg', errmsg)
logger.error(
'微信生成店铺二维码失败 dianpu=%s errcode=%s errmsg=%s',
dianpu.id, error_info.get('errcode'), errmsg,
)
except Exception:
logger.error('微信生成店铺二维码失败 dianpu=%s status=%s', dianpu.id, wx_resp.status_code)
return Response(
{'code': 500, 'msg': f'生成二维码失败: {errmsg}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
@@ -713,10 +734,10 @@ class ShopQrcodeView(APIView):
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 提取相对路径(根据您的 upload_to_oss 返回值格式调整
cos_domain = getattr(settings, 'COS_DOMAIN', '')
# 提取相对路径(与商品图片上传逻辑一致
cos_domain = getattr(settings, 'COS_DOMAIN', '').rstrip('/')
if cos_domain and full_url.startswith(cos_domain):
relative_path = full_url[len(cos_domain):].lstrip('/')
relative_path = full_url.replace(f'{cos_domain}/', '', 1)
else:
relative_path = oss_path
@@ -739,33 +760,3 @@ class ShopQrcodeView(APIView):
'data': {'erweima_url': relative_path}
})
def _get_wx_access_token(self):
"""
获取微信小程序 access_token带缓存
缓存键: wx_mini_access_token
"""
cache_key = 'wx_mini_access_token'
token = cache.get(cache_key)
if token:
return token
appid = getattr(settings, 'WEIXIN_APPID', '')
secret = getattr(settings, 'WEIXIN_SECRET', '')
if not appid or not secret:
logger.error("微信小程序 AppID 或 Secret 未配置")
return None
url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}'
try:
resp = requests.get(url, timeout=5)
data = resp.json()
token = data.get('access_token')
expires_in = data.get('expires_in', 7200)
if token:
# 提前200秒过期防止边界情况
cache.set(cache_key, token, expires_in - 200)
return token
except Exception as e:
logger.error(f"获取微信 access_token 异常: {e}")
return None

View File

@@ -221,8 +221,8 @@ class UserInfoUpdateView(APIView):
else:
code = str(code or '').strip()
# 认证页仅上传头像+手机号,不带昵称
is_phone_auth_flow = bool(av) and not nick
# 手机号强制认证页:同时带头像 + shoujihao_code且不带 nicheng资料编辑页会带昵称
is_phone_auth_flow = bool(av) and bool(code) and not nick
phone_error = None
if is_phone_auth_flow and not code:

View File

@@ -184,6 +184,88 @@ def _upload_to_aliyun_oss(file_obj, file_path):
return None
def _url_to_oss_key(file_url):
"""从完整 URL 或相对路径提取 OSS 对象 Key"""
if not file_url:
return None
file_url = str(file_url).strip()
if not file_url:
return None
oss_type = getattr(settings, 'OSS_TYPE', 'tencent')
if oss_type == 'tencent':
domain = getattr(settings, 'COS_DOMAIN', '')
elif oss_type == 'aliyun':
domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '')
else:
return None
if file_url.startswith('http'):
if domain and domain not in file_url:
return None
prefix = f"{domain.rstrip('/')}/"
if file_url.startswith(prefix):
return file_url[len(prefix):]
return file_url.split('://', 1)[-1].split('/', 1)[-1]
return file_url.lstrip('/')
def copy_in_oss(src_key_or_url, dest_key):
"""
在 OSS 内复制对象(服务端复制,不经过本地)
src_key_or_url: 源对象 Key 或完整 URL
dest_key: 目标对象 Key相对路径
返回: 成功 True失败 False
"""
try:
src_key = _url_to_oss_key(src_key_or_url)
if not src_key or not dest_key:
return False
dest_key = dest_key.lstrip('/')
oss_type = getattr(settings, 'OSS_TYPE', 'tencent')
if oss_type == 'tencent':
return _copy_in_tencent_cos(src_key, dest_key)
elif oss_type == 'aliyun':
return _copy_in_aliyun_oss(src_key, dest_key)
return False
except Exception as e:
print(f"OSS复制失败: {e}")
return False
def _copy_in_tencent_cos(src_key, dest_key):
client = get_oss_client()
bucket = getattr(settings, 'COS_BUCKET', '')
region = getattr(settings, 'COS_REGION', 'ap-shanghai')
try:
client.copy_object(
Bucket=bucket,
Key=dest_key,
CopySource={
'Bucket': bucket,
'Region': region,
'Key': src_key,
},
)
return True
except Exception as e:
print(f"腾讯云COS复制失败: {e}")
return False
def _copy_in_aliyun_oss(src_key, dest_key):
bucket = get_oss_client()
try:
result = bucket.copy_object(src_key, dest_key)
return result.status == 200
except Exception as e:
print(f"阿里云OSS复制失败: {e}")
return False
def delete_from_oss(file_url):
"""
从OSS删除文件