Browse Source

fix 代充接口权限

zoujiajian 9 hours ago
parent
commit
b787a2f1a7

+ 14 - 0
docs/frontend-security-integration.md

@@ -215,3 +215,17 @@ ip 筛选目前只精确匹配 remoteIp 或 realIp,不查询 forwardedFor 链
 - 沿用既有角色菜单授权,无新增 SQL。平台写权限同时核验后台账号启用状态。
 - spec1.vue 的新增/编辑/删除/批量上架入口与通用 SKU/spec 写请求拦截同步校验;保留合并后的其他页面逻辑。
 - 此次覆盖这两个控制器的写入口,不代表所有商品、批发或增值服务接口已审计完成。需部署前后端,验证无权限管理员直接请求同样被拒绝。
+
+## 代充四项操作权限(2026-10-21)
+- 代充账号管理菜单下新增 recharge:import / recharge:export / recharge:auto / recharge:updateCount。
+- 导入订单及订单号预查询、代充账号与操作统计导出、自动充值解析/确认/状态、单笔和批量及历史次数变更共 10 个接口校验独立权限及有效后台账号。
+- 批量发货按钮实际调用批量变更次数接口,归入 recharge:updateCount;单笔发货及卡密管理未纳入本次四项权限范围。
+- 前端按钮及请求拦截一致,直接下载入口额外检查 export;后端为最终授权边界。
+- 统一权限 SQL 更新至 25 项,只补菜单,不默认授权。重复串行运行后手工勾选权限并保存重登。
+
+## 管理员自定义密码(2026-10-21)
+- 管理员列表新增“修改密码”弹窗(仅 root);原“重置密码”按钮及前端调用已移除,后端 `/manage/user/put/reset/pwd/{id}` 的映射及方法已注释停用。
+- `PUT /manage/user/put/password/{id}`,JSON password/confirmPassword。后端 root + 有效管理员校验;密码 12–64 位,含大小写字母及数字,不含空白,两次一致且不同于旧密码。
+- 只更新目标 password,沿用现有 SHA256 登录兼容格式,注销目标登录会话并刷新缓存;不修改 status、角色或登录锁表。
+- 不给修改密码接口添加请求体日志;操作日志仅记录 operatorId/targetId。前端密码不写入本地缓存,弹窗关闭清空。
+- 无新增菜单 SQL(root 专属)。部署前后端,需测试无权限拒绝、密码生效、旧密码失效、目标会话退出、封禁状态保持。

+ 1 - 1
netflix-web/src/main/java/com/cyksj/aspect/RefundAuthorizationAspect.java

@@ -19,7 +19,7 @@ public class RefundAuthorizationAspect {
     @Around("@annotation(permission)")
     public Object authorize(ProceedingJoinPoint point, SaCheckPermission permission) throws Throwable {
         for (String value : permission.value()) {
-            if (!"order:refund".equals(value) && !"order:refundTransfer".equals(value) && !"order:priceDiffer".equals(value) && !value.startsWith("merchant:") && !value.startsWith("platform:")) continue;
+            if (!"order:refund".equals(value) && !"order:refundTransfer".equals(value) && !"order:priceDiffer".equals(value) && !value.startsWith("merchant:") && !value.startsWith("platform:") && !value.startsWith("recharge:")) continue;
             StpUtil.checkLogin();
             StpUtil.checkPermission(value);
             CmsUser admin = cmsUserMapper.selectById(StpUtil.getLoginIdAsLong());

+ 46 - 10
netflix-web/src/main/java/com/cyksj/web/controller/manage/cms/CmsAdminUserController.java

@@ -276,16 +276,52 @@ public class CmsAdminUserController {
 		return GatewayResponse.SUCCESS.newBuilder().toResult("用户状态变更成功");
 	}
 
-	@SneakyThrows
-	@SaCheckRole("root")
-    @PutMapping("/put/reset/pwd/{id}")
-	public Result<String> resetPwd(@PathVariable("id") Integer id) {
-		CmsUser cmsUser = cmsUserMapper.selectById(id);
-		cmsUser.setPassword(Codec.DoDigest.custom().setAlgorithm(Codec.DoDigest.Algorithm.SHA256).setStringData(Constant.INITIAL_PASSWORD).toBase64String());
-		cmsUserMapper.updateById(cmsUser);
-		cacheService.saveUser(cmsUser.getId());
-		return GatewayResponse.SUCCESS.newBuilder().toResult("重置密码成功");
-	}
+    // Disabled: fixed-password reset replaced by explicit administrator password change.
+// 	@SneakyThrows
+// 	@SaCheckRole("root")
+//     @PutMapping("/put/reset/pwd/{id}")
+// 	public Result<String> resetPwd(@PathVariable("id") Integer id) {
+// 		CmsUser cmsUser = cmsUserMapper.selectById(id);
+// 		cmsUser.setPassword(Codec.DoDigest.custom().setAlgorithm(Codec.DoDigest.Algorithm.SHA256).setStringData(Constant.INITIAL_PASSWORD).toBase64String());
+// 		cmsUserMapper.updateById(cmsUser);
+// 		cacheService.saveUser(cmsUser.getId());
+// 		return GatewayResponse.SUCCESS.newBuilder().toResult("重置密码成功");
+// 	}
+
+    /** Root sets an administrator password; no password/request-body audit logging. */
+    @SaCheckRole("root")
+    @PutMapping("/put/password/{id}")
+    public Result<String> changeAdminPassword(@PathVariable Long id,
+            @RequestBody java.util.Map<String, String> body) throws Exception {
+        CmsUser operator = cmsUserMapper.selectById(StpUtil.getLoginIdAsLong());
+        if (operator == null || !Boolean.TRUE.equals(operator.getStatus())) {
+            throw BusinessRuntimeException.getInstance("后台账号不存在或已停用");
+        }
+        String password = body.get("password");
+        if (password == null || password.length() < 12 || password.length() > 64
+                || password.chars().anyMatch(Character::isWhitespace)
+                || !password.matches(".*[a-z].*") || !password.matches(".*[A-Z].*")
+                || !password.matches(".*[0-9].*")) {
+            throw BusinessRuntimeException.getInstance("密码须为12至64位,包含大小写字母和数字,不能包含空白字符");
+        }
+        if (!password.equals(body.get("confirmPassword"))) {
+            throw BusinessRuntimeException.getInstance("两次输入的密码不一致");
+        }
+        CmsUser target = id == null || id <= 0 ? null : cmsUserMapper.selectById(id);
+        if (target == null) throw BusinessRuntimeException.getInstance("管理员不存在");
+        String encoded = Codec.DoDigest.custom().setAlgorithm(Codec.DoDigest.Algorithm.SHA256)
+                .setStringData(password).toBase64String();
+        if (encoded.equals(target.getPassword())) throw BusinessRuntimeException.getInstance("新密码不能与原密码相同");
+        CmsUser update = new CmsUser();
+        update.setId(id);
+        update.setPassword(encoded);
+        if (cmsUserMapper.updateById(update) != 1) throw BusinessRuntimeException.getInstance("修改密码失败");
+        StpUtil.logoutByLoginId(id);
+        cacheService.saveUser(id);
+        // Keep status and admin_login_guard untouched: password changes never unlock accounts.
+        log.info("Administrator password changed operatorId={} targetId={}", operator.getId(), id);
+        return GatewayResponse.SUCCESS.newBuilder().toResult("修改密码成功,请使用新密码重新登录");
+    }
 
 	/**
 	 * 手机号获取登录信息

+ 12 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/group/CmsGroupRelationController.java

@@ -1,5 +1,7 @@
 package com.cyksj.web.controller.manage.group;
 
+import cn.dev33.satoken.annotation.SaCheckPermission;
+
 import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DateTime;
@@ -288,6 +290,7 @@ public class CmsGroupRelationController {
     /**
      * 导出代充账号
      */
+    @SaCheckPermission("recharge:export")
     @GetMapping("/export/recharge")
     public void getRechargeList(String rechargeStatus, String orderNo, Boolean isVipError, HttpServletResponse response) {
         MapBuilder builder = MapUtils.flatBuilder(request.getParameterMap());
@@ -455,6 +458,7 @@ public class CmsGroupRelationController {
     /**
      * GPT代充 设置剩余次数
      */
+    @SaCheckPermission("recharge:updateCount")
     @PutMapping("/recharge/update/remain/num")
     public Result<String> updateRechargeRemainNum(@RequestBody RechargeRemainNumReq req) {
         long adminId = StpUtil.getLoginIdAsLong();
@@ -486,6 +490,7 @@ public class CmsGroupRelationController {
     /**
      * 批量处理GPT代充次数
      */
+    @SaCheckPermission("recharge:updateCount")
     @PostMapping("/batch/recharge/update/remain/num")
     public Result<List<BatchRechargeRemainNumResp>> batchUpdateRechargeRemainNum(@RequestBody BatchRechargeRemainNumReq req) {
         long adminId = StpUtil.getLoginIdAsLong();
@@ -496,6 +501,7 @@ public class CmsGroupRelationController {
     /**
      * 更新剩余次数 针对之前的代充账号
      */
+    @SaCheckPermission("recharge:updateCount")
     @PostMapping("/recharge/set/remainNumBf")
     public Result<String> setRechargeRemainNumBf(@RequestBody RechargeRemainNumReq req) {
         long adminId = StpUtil.getLoginIdAsLong();
@@ -574,6 +580,7 @@ public class CmsGroupRelationController {
     /**
      * 导出 代充操作数据
      */
+    @SaCheckPermission("recharge:export")
     @GetMapping("/export/recharge/operate/statistics/data")
     public void exportRechargeStatisticsData(@RequestParam(name = "goodsIds", required = false) List<Long> goodsIds, String startTime, String endTime, String operator, HttpServletResponse response) {
         if (StrUtil.isEmpty(startTime)) {
@@ -765,6 +772,7 @@ public class CmsGroupRelationController {
     /**
      * 验证GPT代充凭证
      */
+    @SaCheckPermission("recharge:auto")
     @PostMapping("/parse-token")
     public Result<TokenParseResult> parseToken(@RequestBody ParseTokenRequest request) {
         TokenParseResult tokenParseResult = gptRechargeService.parseToken(request.getAccessToken());
@@ -774,6 +782,7 @@ public class CmsGroupRelationController {
     /**
      * 确认充值
      */
+    @SaCheckPermission("recharge:auto")
     @PostMapping("/confirm")
     public Result<String> confirmRecharge(@RequestBody @Validated GroupsRelationRechargeReq req) {
         long adminId = StpUtil.getLoginIdAsLong();
@@ -785,6 +794,7 @@ public class CmsGroupRelationController {
     /**
      * 查询任务状态
      */
+    @SaCheckPermission("recharge:auto")
     @GetMapping("/task-status/{taskId}")
     public Result<TaskResult> getTaskStatus(@PathVariable String taskId) {
         return GatewayResponse.SUCCESS.newBuilder().toResult(gptRechargeService.getTaskResult(taskId));
@@ -793,6 +803,7 @@ public class CmsGroupRelationController {
     /**
      * 订单号车票详情
      */
+    @SaCheckPermission("recharge:import")
     @GetMapping("/get/order/relation")
     public Result<GroupsRelation> getOrderRelation(String orderNo) {
         GroupsRelation relation = gptRechargeService.getOrderRelation(orderNo);
@@ -802,6 +813,7 @@ public class CmsGroupRelationController {
     /**
      * GPT独立会员导入代充列表
      */
+    @SaCheckPermission("recharge:import")
     @PostMapping("/gpt/independent/export/recharge")
     @Log(module = "账号管理/用户代充/导入GPT独立会员订单", businessType = BusinessType.POST, isSaveRequestData = true)
     public Result<String> gptIndependentExportRecharge(@RequestBody GptIndependentExportRechargeReq rechargeReq) {