zoujiajian há 10 horas atrás
pai
commit
6161c43584

+ 5 - 0
docs/exchange-code-security.md

@@ -49,3 +49,8 @@
 - 返回前用 Java 严格比较原码,避免数据库不区分大小写的匹配;响应不再包含 code 字段。前端应使用自己的输入码执行兑换,不依赖详情响应回填。
 - 不修改兑换核销流程,不增加数据库迁移。此改动未新增限流,不代表全部兑换安全问题已完成。
 - 部署验收:空码与错误码不返回数据;完整有效测试码正常展示;附加参数不能改变查询目标;响应不含完整兑换码。
+
+## 2026-10-21 外部镜像查询访问策略调整
+按用户要求,`GET /external/app/get/mirror` 恢复公开查询,移除该查询的后台登录/root/兑换码查询权限限制。固定规格、参数白名单和分页上限保留。
+`PUT /external/app/refund/{id}` 仍要求后台登录、root 和 exchangeCode:refund;类级权限移至退款方法,未取消退款授权。
+此为公开访问策略,不代表返回的完整镜像兑换码属于保密数据;未修改用户端兑换码详情的精确码校验。

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

@@ -180,3 +180,19 @@ ip 筛选目前只精确匹配 remoteIp 或 realIp,不查询 forwardedFor 链
 - 前端路由、页面和请求拦截同步校验该权限。该权限可查询全部登录日志,应仅授权可信审计人员。
 - 执行 `docs/sql/20261021-login-audit-menu.sql`,核对系统管理父菜单 ID;仅默认授权 root,其他角色手工勾选菜单并刷新权限、重新登录。
 - 验收:授权角色可查询;无权限角色无法进入且直接调用接口被拒绝;未登录拒绝;旧地址同样受控。
+
+## 2026-10-21 联调反馈调整
+- `/sys/get?key=sys_default_shop_key` 撤销额外商户读取权限检查;配置修改接口权限保留。
+- 兑换码列表和导出固定 `ec.id desc`,生成后前端回第一页(保留筛选条件)。
+- `/manage/user/add/balance` 单笔金额暂按截图边界设为小于 30000 元,接口单位为分,`deductBalance >= 3000000` 拒绝;前端同步限制。用户尚未另行指定业务上限。
+- 此上限仅限人工加余额,不是账户总余额上限,不限制扣款,也不替代累计风控。
+- 无数据库迁移;发布前后端。边界:29999.99 元可通过额度检查,30000/30000.01 元拒绝(其他权限/参数条件仍需满足)。
+
+## 管理端错误提示(2026-10-21)
+- 普通请求及兑换码下载显示后端业务 msg;返回 data=requestId=... 时追加错误编号。
+- 普通请求错误提示保留 8 秒、允许关闭;登录失效、HTTP 错误、网络失败、超时均有提示。
+- 后端补充缺少参数、参数类型和 JSON 格式错误提示;字段绑定校验显示字段名,不回显错误输入值。
+- 未知服务异常仍使用通用提示,完整堆栈通过 requestId 在服务日志定位,不向前端开放 SQL/密钥。
+- 新增前端工具 src/utils/apiError.mjs,发布时需一起提交。无 SQL 变更。
+
+后续调整:按用户要求撤回此次前端错误提示增强,删除 apiError.mjs,恢复 axios 和下载工具原有 msg 展示逻辑;前面的前端提示增强说明不再适用。其他安全修复保留。

+ 1 - 1
netflix-dao/src/main/java/com/cyksj/model/excel/ExcelExchangeCodeData.java

@@ -21,7 +21,7 @@ import java.util.Date;
 @Getter
 @Setter
 @SearchBean(tables = "exchange_code ec left join user u on u.id = ec.user_id",
-		where = "ec.type = 0 :condition:")
+		where = "ec.type = 0 :condition:", orderBy = "ec.id desc")
 public class ExcelExchangeCodeData {
 
 	/**

+ 1 - 1
netflix-dao/src/main/java/com/cyksj/model/views/ExchangeCodeView.java

@@ -17,7 +17,7 @@ import java.util.Date;
 @Getter
 @Setter
 @SearchBean(tables = "exchange_code ec left join user u on u.id = ec.user_id",
-		where = "ec.type = 0 :condition:")
+		where = "ec.type = 0 :condition:", orderBy = "ec.id desc")
 public class ExchangeCodeView {
 	@DbField("ec.id")
 	private Long id;

+ 8 - 1
netflix-web/src/main/java/com/cyksj/web/GlobalExceptionHandler.java

@@ -71,6 +71,13 @@ public class GlobalExceptionHandler {
             apiCode = GatewayApiCode.CMS_USER_NO_PERMISSION;
         }
 
+        if (e instanceof org.springframework.web.bind.MissingServletRequestParameterException) {
+            message = "缺少必填参数:" + ((org.springframework.web.bind.MissingServletRequestParameterException) e).getParameterName();
+        } else if (e instanceof org.springframework.web.method.annotation.MethodArgumentTypeMismatchException) {
+            message = "参数格式不正确:" + ((org.springframework.web.method.annotation.MethodArgumentTypeMismatchException) e).getName();
+        } else if (e instanceof org.springframework.http.converter.HttpMessageNotReadableException) {
+            message = "请求体格式不正确,请检查 JSON 和字段类型";
+        }
         if (apiCode != null) {
             return GatewayResponse.FAIL.newBuilder().buildGatewayCode(apiCode).toResult();
         }
@@ -115,7 +122,7 @@ public class GlobalExceptionHandler {
             Set<String> errorInfoSet = new TreeSet<String>();
             for (FieldError fieldError : fieldErrorList) {
                 // 遍历错误字段信息
-                errorInfoSet.add(fieldError.getDefaultMessage());
+                errorInfoSet.add(fieldError.getField() + ":" + (fieldError.isBindingFailure() ? "格式或类型不正确" : fieldError.getDefaultMessage()));
             }
 
             StringBuilder sbf = new StringBuilder();

+ 2 - 3
netflix-web/src/main/java/com/cyksj/web/controller/external/ExternalController.java

@@ -48,8 +48,6 @@ import java.util.List;
  * 创建时间:2024/6/11 15:01
  */
 @RestController
-@SaCheckLogin
-@SaCheckRole("root")
 @RequestMapping("/external/app")
 @RequiredArgsConstructor
 public class ExternalController {
@@ -75,7 +73,6 @@ public class ExternalController {
      * 知乎 特殊镜像规格 兑换码列表
      */
     @GetMapping("/get/mirror")
-    @SaCheckPermission("exchangeCode:view")
     public Result<SearchResult<ExternalExchangeCodeView>> getMirror(@RequestParam(required = true, defaultValue = "18") Long gid) {
         //目前只展示规格id 444 GPT基础镜像2月
         List<Long> showSkuId = null;
@@ -104,6 +101,8 @@ public class ExternalController {
 	/**
 	 * 退款 成功 重新生成兑换码
 	 */
+	@SaCheckLogin
+	@SaCheckRole("root")
 	@PutMapping("/refund/{id}")
 	@SaCheckPermission("exchangeCode:refund")
 	@Log(module = "兑换码/外部退款重发", businessType = BusinessType.PUT, isSaveRequestData = true)

+ 4 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/CmsUserController.java

@@ -480,6 +480,10 @@ public class CmsUserController{
         if (deductBalance.compareTo(BigDecimal.ZERO) <= 0) {
             throw BusinessRuntimeException.getInstance("请增加正确的余额数值");
         }
+        // Stored and requested amounts are integer cents: 30000 yuan = 3000000 cents.
+        if (deductBalance.compareTo(new BigDecimal("3000000")) >= 0) {
+            throw BusinessRuntimeException.getInstance("单笔增加余额必须小于30000元");
+        }
         if (!balanceIdempotency.begin(adminId, "add", requestId, balanceReq)) return GatewayResponse.SUCCESS.newBuilder().toResult();
         Long userId = balanceReq.getUserId();
         User user = userMapper.selectById(userId);

+ 1 - 1
netflix-web/src/main/java/com/cyksj/web/controller/sys/SysConfigController.java

@@ -76,7 +76,7 @@ public class SysConfigController {
 
 	@GetMapping("/get")
 	public Result<SysConfig> get(String key) {
-        checkMerchantConfig(key, "merchant:view");
+        // Configuration read is public; write operations retain merchant authorization.
 		try {
 			String cacheKey = RedisService.key.SYS_CONFIG_CACHE_KEY.getName() + key;
 			Object o = redisService.get(cacheKey);