From 35cc034af10b3d38de9f7fc26100f7bf324a03dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sat, 28 Mar 2026 21:33:42 +0800 Subject: [PATCH 01/21] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90=E5=B7=A5=E5=85=B7=E5=8F=8A?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=AE=9E=E4=BD=93=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AppGenerator 代码生成工具类,支持多平台模板生成 - 生成 app_credential 应用密钥凭证的完整CRUD功能模块 - 生成 app_event 应用操作动态的完整CRUD功能模块 - 添加对应的 Entity、Controller、Service、Mapper 和 Param 类 - 配置多平台模板支持(Vue、UniApp、移动端页面) - 实现 app.config.ts 自动更新功能 - 优化后端实现指南文档说明 --- docs/ai/后端实现指南.md | 3 +- ...t_mp_customer_step1_4_approval_columns.sql | 24 ++ .../controller/AppCredentialController.java | 129 ++++++ .../app/controller/AppEventController.java | 129 ++++++ .../app/controller/AppUserController.java | 129 ++++++ .../app/controller/AppVersionController.java | 129 ++++++ .../gxwebsoft/app/entity/AppCredential.java | 80 ++++ .../com/gxwebsoft/app/entity/AppEvent.java | 76 ++++ .../com/gxwebsoft/app/entity/AppUser.java | 67 +++ .../com/gxwebsoft/app/entity/AppVersion.java | 85 ++++ .../app/mapper/AppCredentialMapper.java | 37 ++ .../gxwebsoft/app/mapper/AppEventMapper.java | 37 ++ .../gxwebsoft/app/mapper/AppUserMapper.java | 37 ++ .../app/mapper/AppVersionMapper.java | 37 ++ .../app/mapper/xml/AppCredentialMapper.xml | 78 ++++ .../app/mapper/xml/AppEventMapper.xml | 72 ++++ .../app/mapper/xml/AppUserMapper.xml | 63 +++ .../app/mapper/xml/AppVersionMapper.xml | 81 ++++ .../app/param/AppCredentialParam.java | 71 ++++ .../gxwebsoft/app/param/AppEventParam.java | 68 ++++ .../com/gxwebsoft/app/param/AppUserParam.java | 58 +++ .../gxwebsoft/app/param/AppVersionParam.java | 78 ++++ .../app/service/AppCredentialService.java | 42 ++ .../app/service/AppEventService.java | 42 ++ .../gxwebsoft/app/service/AppUserService.java | 42 ++ .../app/service/AppVersionService.java | 42 ++ .../impl/AppCredentialServiceImpl.java | 47 +++ .../app/service/impl/AppEventServiceImpl.java | 47 +++ .../app/service/impl/AppUserServiceImpl.java | 47 +++ .../service/impl/AppVersionServiceImpl.java | 47 +++ .../com/gxwebsoft/generator/AppGenerator.java | 384 ++++++++++++++++++ 31 files changed, 2307 insertions(+), 1 deletion(-) create mode 100644 docs/sql/credit_mp_customer_step1_4_approval_columns.sql create mode 100644 src/main/java/com/gxwebsoft/app/controller/AppCredentialController.java create mode 100644 src/main/java/com/gxwebsoft/app/controller/AppEventController.java create mode 100644 src/main/java/com/gxwebsoft/app/controller/AppUserController.java create mode 100644 src/main/java/com/gxwebsoft/app/controller/AppVersionController.java create mode 100644 src/main/java/com/gxwebsoft/app/entity/AppCredential.java create mode 100644 src/main/java/com/gxwebsoft/app/entity/AppEvent.java create mode 100644 src/main/java/com/gxwebsoft/app/entity/AppUser.java create mode 100644 src/main/java/com/gxwebsoft/app/entity/AppVersion.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/AppCredentialMapper.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/AppEventMapper.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/AppUserMapper.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/AppVersionMapper.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/xml/AppCredentialMapper.xml create mode 100644 src/main/java/com/gxwebsoft/app/mapper/xml/AppEventMapper.xml create mode 100644 src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml create mode 100644 src/main/java/com/gxwebsoft/app/mapper/xml/AppVersionMapper.xml create mode 100644 src/main/java/com/gxwebsoft/app/param/AppCredentialParam.java create mode 100644 src/main/java/com/gxwebsoft/app/param/AppEventParam.java create mode 100644 src/main/java/com/gxwebsoft/app/param/AppUserParam.java create mode 100644 src/main/java/com/gxwebsoft/app/param/AppVersionParam.java create mode 100644 src/main/java/com/gxwebsoft/app/service/AppCredentialService.java create mode 100644 src/main/java/com/gxwebsoft/app/service/AppEventService.java create mode 100644 src/main/java/com/gxwebsoft/app/service/AppUserService.java create mode 100644 src/main/java/com/gxwebsoft/app/service/AppVersionService.java create mode 100644 src/main/java/com/gxwebsoft/app/service/impl/AppCredentialServiceImpl.java create mode 100644 src/main/java/com/gxwebsoft/app/service/impl/AppEventServiceImpl.java create mode 100644 src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java create mode 100644 src/main/java/com/gxwebsoft/app/service/impl/AppVersionServiceImpl.java create mode 100644 src/test/java/com/gxwebsoft/generator/AppGenerator.java diff --git a/docs/ai/后端实现指南.md b/docs/ai/后端实现指南.md index c4bde09..e9f7e38 100644 --- a/docs/ai/后端实现指南.md +++ b/docs/ai/后端实现指南.md @@ -9,7 +9,8 @@ ### 1. 修改 credit_mp_customer 表结构 ```sql --- 为第5-7步添加字段(第1-4步字段已存在) +-- 为第5-7步添加字段(第1-4步基础字段已存在;如未包含审核时间/审核人字段,请先补齐 step1-4 的 approved_at/approved_by) +-- 参考:docs/sql/credit_mp_customer_step1_4_approval_columns.sql -- 第5步:合同签订 ALTER TABLE credit_mp_customer ADD COLUMN follow_step5_submitted TINYINT DEFAULT 0 COMMENT '是否已提交'; ALTER TABLE credit_mp_customer ADD COLUMN follow_step5_submitted_at VARCHAR(255) COMMENT '提交时间'; diff --git a/docs/sql/credit_mp_customer_step1_4_approval_columns.sql b/docs/sql/credit_mp_customer_step1_4_approval_columns.sql new file mode 100644 index 0000000..9dbbe0f --- /dev/null +++ b/docs/sql/credit_mp_customer_step1_4_approval_columns.sql @@ -0,0 +1,24 @@ +-- 修复:审核接口(updateStepApproval)会写入 follow_step{1..4}_approved_at / follow_step{1..4}_approved_by +-- 若数据库缺少这些列,会触发:Unknown column 'follow_step1_approved_at' in 'field list' +-- +-- 建议先检查: +-- SHOW COLUMNS FROM credit_mp_customer LIKE 'follow_step%_approved%'; +-- +-- 然后按需执行下面的 ALTER TABLE(如果你的 MySQL 版本支持,也可以改为 ADD COLUMN IF NOT EXISTS)。 + +-- 第1步:案件受理 +ALTER TABLE credit_mp_customer ADD COLUMN follow_step1_approved_at VARCHAR(255) NULL COMMENT '第1步审核时间'; +ALTER TABLE credit_mp_customer ADD COLUMN follow_step1_approved_by BIGINT NULL COMMENT '第1步审核人ID'; + +-- 第2步:材料准备 +ALTER TABLE credit_mp_customer ADD COLUMN follow_step2_approved_at VARCHAR(255) NULL COMMENT '第2步审核时间'; +ALTER TABLE credit_mp_customer ADD COLUMN follow_step2_approved_by BIGINT NULL COMMENT '第2步审核人ID'; + +-- 第3步:案件办理 +ALTER TABLE credit_mp_customer ADD COLUMN follow_step3_approved_at VARCHAR(255) NULL COMMENT '第3步审核时间'; +ALTER TABLE credit_mp_customer ADD COLUMN follow_step3_approved_by BIGINT NULL COMMENT '第3步审核人ID'; + +-- 第4步:送达签收 +ALTER TABLE credit_mp_customer ADD COLUMN follow_step4_approved_at VARCHAR(255) NULL COMMENT '第4步审核时间'; +ALTER TABLE credit_mp_customer ADD COLUMN follow_step4_approved_by BIGINT NULL COMMENT '第4步审核人ID'; + diff --git a/src/main/java/com/gxwebsoft/app/controller/AppCredentialController.java b/src/main/java/com/gxwebsoft/app/controller/AppCredentialController.java new file mode 100644 index 0000000..17e68da --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/controller/AppCredentialController.java @@ -0,0 +1,129 @@ +package com.gxwebsoft.app.controller; + +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.app.service.AppCredentialService; +import com.gxwebsoft.app.entity.AppCredential; +import com.gxwebsoft.app.param.AppCredentialParam; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.BatchParam; +import com.gxwebsoft.common.core.annotation.OperationLog; +import com.gxwebsoft.common.system.entity.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 应用密钥凭证控制器 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Tag(name = "应用密钥凭证管理") +@RestController +@RequestMapping("/api/app/app-credential") +public class AppCredentialController extends BaseController { + @Resource + private AppCredentialService appCredentialService; + + @PreAuthorize("hasAuthority('app:appCredential:list')") + @Operation(summary = "分页查询应用密钥凭证") + @GetMapping("/page") + public ApiResult> page(AppCredentialParam param) { + // 使用关联查询 + return success(appCredentialService.pageRel(param)); + } + + @PreAuthorize("hasAuthority('app:appCredential:list')") + @Operation(summary = "查询全部应用密钥凭证") + @GetMapping() + public ApiResult> list(AppCredentialParam param) { + // 使用关联查询 + return success(appCredentialService.listRel(param)); + } + + @PreAuthorize("hasAuthority('app:appCredential:list')") + @Operation(summary = "根据id查询应用密钥凭证") + @GetMapping("/{id}") + public ApiResult get(@PathVariable("id") Integer id) { + // 使用关联查询 + return success(appCredentialService.getByIdRel(id)); + } + + @PreAuthorize("hasAuthority('app:appCredential:save')") + @OperationLog + @Operation(summary = "添加应用密钥凭证") + @PostMapping() + public ApiResult save(@RequestBody AppCredential appCredential) { + // 记录当前登录用户id + User loginUser = getLoginUser(); + if (loginUser != null) { + appCredential.setUserId(loginUser.getUserId()); + } + if (appCredentialService.save(appCredential)) { + return success("添加成功"); + } + return fail("添加失败"); + } + + @PreAuthorize("hasAuthority('app:appCredential:update')") + @OperationLog + @Operation(summary = "修改应用密钥凭证") + @PutMapping() + public ApiResult update(@RequestBody AppCredential appCredential) { + if (appCredentialService.updateById(appCredential)) { + return success("修改成功"); + } + return fail("修改失败"); + } + + @PreAuthorize("hasAuthority('app:appCredential:remove')") + @OperationLog + @Operation(summary = "删除应用密钥凭证") + @DeleteMapping("/{id}") + public ApiResult remove(@PathVariable("id") Integer id) { + if (appCredentialService.removeById(id)) { + return success("删除成功"); + } + return fail("删除失败"); + } + + @PreAuthorize("hasAuthority('app:appCredential:save')") + @OperationLog + @Operation(summary = "批量添加应用密钥凭证") + @PostMapping("/batch") + public ApiResult saveBatch(@RequestBody List list) { + if (appCredentialService.saveBatch(list)) { + return success("添加成功"); + } + return fail("添加失败"); + } + + @PreAuthorize("hasAuthority('app:appCredential:update')") + @OperationLog + @Operation(summary = "批量修改应用密钥凭证") + @PutMapping("/batch") + public ApiResult removeBatch(@RequestBody BatchParam batchParam) { + if (batchParam.update(appCredentialService, "id")) { + return success("修改成功"); + } + return fail("修改失败"); + } + + @PreAuthorize("hasAuthority('app:appCredential:remove')") + @OperationLog + @Operation(summary = "批量删除应用密钥凭证") + @DeleteMapping("/batch") + public ApiResult removeBatch(@RequestBody List ids) { + if (appCredentialService.removeByIds(ids)) { + return success("删除成功"); + } + return fail("删除失败"); + } + +} diff --git a/src/main/java/com/gxwebsoft/app/controller/AppEventController.java b/src/main/java/com/gxwebsoft/app/controller/AppEventController.java new file mode 100644 index 0000000..84a7413 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/controller/AppEventController.java @@ -0,0 +1,129 @@ +package com.gxwebsoft.app.controller; + +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.app.service.AppEventService; +import com.gxwebsoft.app.entity.AppEvent; +import com.gxwebsoft.app.param.AppEventParam; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.BatchParam; +import com.gxwebsoft.common.core.annotation.OperationLog; +import com.gxwebsoft.common.system.entity.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 应用操作动态控制器 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Tag(name = "应用操作动态管理") +@RestController +@RequestMapping("/api/app/app-event") +public class AppEventController extends BaseController { + @Resource + private AppEventService appEventService; + + @PreAuthorize("hasAuthority('app:appEvent:list')") + @Operation(summary = "分页查询应用操作动态") + @GetMapping("/page") + public ApiResult> page(AppEventParam param) { + // 使用关联查询 + return success(appEventService.pageRel(param)); + } + + @PreAuthorize("hasAuthority('app:appEvent:list')") + @Operation(summary = "查询全部应用操作动态") + @GetMapping() + public ApiResult> list(AppEventParam param) { + // 使用关联查询 + return success(appEventService.listRel(param)); + } + + @PreAuthorize("hasAuthority('app:appEvent:list')") + @Operation(summary = "根据id查询应用操作动态") + @GetMapping("/{id}") + public ApiResult get(@PathVariable("id") Integer id) { + // 使用关联查询 + return success(appEventService.getByIdRel(id)); + } + + @PreAuthorize("hasAuthority('app:appEvent:save')") + @OperationLog + @Operation(summary = "添加应用操作动态") + @PostMapping() + public ApiResult save(@RequestBody AppEvent appEvent) { + // 记录当前登录用户id + User loginUser = getLoginUser(); + if (loginUser != null) { + appEvent.setUserId(loginUser.getUserId()); + } + if (appEventService.save(appEvent)) { + return success("添加成功"); + } + return fail("添加失败"); + } + + @PreAuthorize("hasAuthority('app:appEvent:update')") + @OperationLog + @Operation(summary = "修改应用操作动态") + @PutMapping() + public ApiResult update(@RequestBody AppEvent appEvent) { + if (appEventService.updateById(appEvent)) { + return success("修改成功"); + } + return fail("修改失败"); + } + + @PreAuthorize("hasAuthority('app:appEvent:remove')") + @OperationLog + @Operation(summary = "删除应用操作动态") + @DeleteMapping("/{id}") + public ApiResult remove(@PathVariable("id") Integer id) { + if (appEventService.removeById(id)) { + return success("删除成功"); + } + return fail("删除失败"); + } + + @PreAuthorize("hasAuthority('app:appEvent:save')") + @OperationLog + @Operation(summary = "批量添加应用操作动态") + @PostMapping("/batch") + public ApiResult saveBatch(@RequestBody List list) { + if (appEventService.saveBatch(list)) { + return success("添加成功"); + } + return fail("添加失败"); + } + + @PreAuthorize("hasAuthority('app:appEvent:update')") + @OperationLog + @Operation(summary = "批量修改应用操作动态") + @PutMapping("/batch") + public ApiResult removeBatch(@RequestBody BatchParam batchParam) { + if (batchParam.update(appEventService, "id")) { + return success("修改成功"); + } + return fail("修改失败"); + } + + @PreAuthorize("hasAuthority('app:appEvent:remove')") + @OperationLog + @Operation(summary = "批量删除应用操作动态") + @DeleteMapping("/batch") + public ApiResult removeBatch(@RequestBody List ids) { + if (appEventService.removeByIds(ids)) { + return success("删除成功"); + } + return fail("删除失败"); + } + +} diff --git a/src/main/java/com/gxwebsoft/app/controller/AppUserController.java b/src/main/java/com/gxwebsoft/app/controller/AppUserController.java new file mode 100644 index 0000000..3a7f660 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/controller/AppUserController.java @@ -0,0 +1,129 @@ +package com.gxwebsoft.app.controller; + +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.app.service.AppUserService; +import com.gxwebsoft.app.entity.AppUser; +import com.gxwebsoft.app.param.AppUserParam; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.BatchParam; +import com.gxwebsoft.common.core.annotation.OperationLog; +import com.gxwebsoft.common.system.entity.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 应用成员控制器 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Tag(name = "应用成员管理") +@RestController +@RequestMapping("/api/app/app-user") +public class AppUserController extends BaseController { + @Resource + private AppUserService appUserService; + + @PreAuthorize("hasAuthority('app:appUser:list')") + @Operation(summary = "分页查询应用成员") + @GetMapping("/page") + public ApiResult> page(AppUserParam param) { + // 使用关联查询 + return success(appUserService.pageRel(param)); + } + + @PreAuthorize("hasAuthority('app:appUser:list')") + @Operation(summary = "查询全部应用成员") + @GetMapping() + public ApiResult> list(AppUserParam param) { + // 使用关联查询 + return success(appUserService.listRel(param)); + } + + @PreAuthorize("hasAuthority('app:appUser:list')") + @Operation(summary = "根据id查询应用成员") + @GetMapping("/{id}") + public ApiResult get(@PathVariable("id") Integer id) { + // 使用关联查询 + return success(appUserService.getByIdRel(id)); + } + + @PreAuthorize("hasAuthority('app:appUser:save')") + @OperationLog + @Operation(summary = "添加应用成员") + @PostMapping() + public ApiResult save(@RequestBody AppUser appUser) { + // 记录当前登录用户id + User loginUser = getLoginUser(); + if (loginUser != null) { + appUser.setUserId(loginUser.getUserId()); + } + if (appUserService.save(appUser)) { + return success("添加成功"); + } + return fail("添加失败"); + } + + @PreAuthorize("hasAuthority('app:appUser:update')") + @OperationLog + @Operation(summary = "修改应用成员") + @PutMapping() + public ApiResult update(@RequestBody AppUser appUser) { + if (appUserService.updateById(appUser)) { + return success("修改成功"); + } + return fail("修改失败"); + } + + @PreAuthorize("hasAuthority('app:appUser:remove')") + @OperationLog + @Operation(summary = "删除应用成员") + @DeleteMapping("/{id}") + public ApiResult remove(@PathVariable("id") Integer id) { + if (appUserService.removeById(id)) { + return success("删除成功"); + } + return fail("删除失败"); + } + + @PreAuthorize("hasAuthority('app:appUser:save')") + @OperationLog + @Operation(summary = "批量添加应用成员") + @PostMapping("/batch") + public ApiResult saveBatch(@RequestBody List list) { + if (appUserService.saveBatch(list)) { + return success("添加成功"); + } + return fail("添加失败"); + } + + @PreAuthorize("hasAuthority('app:appUser:update')") + @OperationLog + @Operation(summary = "批量修改应用成员") + @PutMapping("/batch") + public ApiResult removeBatch(@RequestBody BatchParam batchParam) { + if (batchParam.update(appUserService, "id")) { + return success("修改成功"); + } + return fail("修改失败"); + } + + @PreAuthorize("hasAuthority('app:appUser:remove')") + @OperationLog + @Operation(summary = "批量删除应用成员") + @DeleteMapping("/batch") + public ApiResult removeBatch(@RequestBody List ids) { + if (appUserService.removeByIds(ids)) { + return success("删除成功"); + } + return fail("删除失败"); + } + +} diff --git a/src/main/java/com/gxwebsoft/app/controller/AppVersionController.java b/src/main/java/com/gxwebsoft/app/controller/AppVersionController.java new file mode 100644 index 0000000..c5e8ad2 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/controller/AppVersionController.java @@ -0,0 +1,129 @@ +package com.gxwebsoft.app.controller; + +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.app.service.AppVersionService; +import com.gxwebsoft.app.entity.AppVersion; +import com.gxwebsoft.app.param.AppVersionParam; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.BatchParam; +import com.gxwebsoft.common.core.annotation.OperationLog; +import com.gxwebsoft.common.system.entity.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 应用版本发布记录控制器 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Tag(name = "应用版本发布记录管理") +@RestController +@RequestMapping("/api/app/app-version") +public class AppVersionController extends BaseController { + @Resource + private AppVersionService appVersionService; + + @PreAuthorize("hasAuthority('app:appVersion:list')") + @Operation(summary = "分页查询应用版本发布记录") + @GetMapping("/page") + public ApiResult> page(AppVersionParam param) { + // 使用关联查询 + return success(appVersionService.pageRel(param)); + } + + @PreAuthorize("hasAuthority('app:appVersion:list')") + @Operation(summary = "查询全部应用版本发布记录") + @GetMapping() + public ApiResult> list(AppVersionParam param) { + // 使用关联查询 + return success(appVersionService.listRel(param)); + } + + @PreAuthorize("hasAuthority('app:appVersion:list')") + @Operation(summary = "根据id查询应用版本发布记录") + @GetMapping("/{id}") + public ApiResult get(@PathVariable("id") Integer id) { + // 使用关联查询 + return success(appVersionService.getByIdRel(id)); + } + + @PreAuthorize("hasAuthority('app:appVersion:save')") + @OperationLog + @Operation(summary = "添加应用版本发布记录") + @PostMapping() + public ApiResult save(@RequestBody AppVersion appVersion) { + // 记录当前登录用户id + User loginUser = getLoginUser(); + if (loginUser != null) { + appVersion.setUserId(loginUser.getUserId()); + } + if (appVersionService.save(appVersion)) { + return success("添加成功"); + } + return fail("添加失败"); + } + + @PreAuthorize("hasAuthority('app:appVersion:update')") + @OperationLog + @Operation(summary = "修改应用版本发布记录") + @PutMapping() + public ApiResult update(@RequestBody AppVersion appVersion) { + if (appVersionService.updateById(appVersion)) { + return success("修改成功"); + } + return fail("修改失败"); + } + + @PreAuthorize("hasAuthority('app:appVersion:remove')") + @OperationLog + @Operation(summary = "删除应用版本发布记录") + @DeleteMapping("/{id}") + public ApiResult remove(@PathVariable("id") Integer id) { + if (appVersionService.removeById(id)) { + return success("删除成功"); + } + return fail("删除失败"); + } + + @PreAuthorize("hasAuthority('app:appVersion:save')") + @OperationLog + @Operation(summary = "批量添加应用版本发布记录") + @PostMapping("/batch") + public ApiResult saveBatch(@RequestBody List list) { + if (appVersionService.saveBatch(list)) { + return success("添加成功"); + } + return fail("添加失败"); + } + + @PreAuthorize("hasAuthority('app:appVersion:update')") + @OperationLog + @Operation(summary = "批量修改应用版本发布记录") + @PutMapping("/batch") + public ApiResult removeBatch(@RequestBody BatchParam batchParam) { + if (batchParam.update(appVersionService, "id")) { + return success("修改成功"); + } + return fail("修改失败"); + } + + @PreAuthorize("hasAuthority('app:appVersion:remove')") + @OperationLog + @Operation(summary = "批量删除应用版本发布记录") + @DeleteMapping("/batch") + public ApiResult removeBatch(@RequestBody List ids) { + if (appVersionService.removeByIds(ids)) { + return success("删除成功"); + } + return fail("删除失败"); + } + +} diff --git a/src/main/java/com/gxwebsoft/app/entity/AppCredential.java b/src/main/java/com/gxwebsoft/app/entity/AppCredential.java new file mode 100644 index 0000000..2384db5 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/entity/AppCredential.java @@ -0,0 +1,80 @@ +package com.gxwebsoft.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.TableLogic; +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应用密钥凭证 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:43 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(name = "AppCredential对象", description = "应用密钥凭证") +public class AppCredential implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "自增ID") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @Schema(description = "关联应用ID") + private Long websiteId; + + @Schema(description = "凭证名称,如生产环境密钥") + private String name; + + @Schema(description = "App ID(公开)") + private String appId; + + @Schema(description = "App Secret(加密存储)") + private String appSecret; + + @Schema(description = "凭证类型: server/client/webhook") + private String type; + + @Schema(description = "权限范围,空格分隔") + private String scopes; + + @Schema(description = "到期时间,NULL=永不过期") + private LocalDateTime expireTime; + + @Schema(description = "最后使用时间") + private LocalDateTime lastUsedAt; + + private String remark; + + @Schema(description = "排序(数字越小越靠前)") + private Integer sortNumber; + + @Schema(description = "状态, 0正常, 1冻结") + private Integer status; + + @Schema(description = "是否删除, 0否, 1是") + @TableLogic + private Integer deleted; + + @Schema(description = "用户ID") + private Integer userId; + + @Schema(description = "租户id") + private Integer tenantId; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "修改时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + +} diff --git a/src/main/java/com/gxwebsoft/app/entity/AppEvent.java b/src/main/java/com/gxwebsoft/app/entity/AppEvent.java new file mode 100644 index 0000000..b3fb88c --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/entity/AppEvent.java @@ -0,0 +1,76 @@ +package com.gxwebsoft.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import java.time.LocalDateTime; +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应用操作动态 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(name = "AppEvent对象", description = "应用操作动态") +public class AppEvent implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "自增ID") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @Schema(description = "关联应用ID") + private Long websiteId; + + @Schema(description = "事件类型: created/published/updated/domain_bound/member_added/status_changed") + private String eventType; + + @Schema(description = "事件标题,如已发布") + private String title; + + @Schema(description = "详细描述") + private String content; + + @Schema(description = "操作人用户ID") + private Long operatorId; + + @Schema(description = "操作人名称(冗余)") + private String operator; + + @Schema(description = "关联ID,如版本ID") + private Long refId; + + @Schema(description = "关联类型") + private String refType; + + @Schema(description = "扩展数据") + private String extra; + + @Schema(description = "排序(数字越小越靠前)") + private Integer sortNumber; + + @Schema(description = "状态, 0正常, 1冻结") + private Integer status; + + @Schema(description = "用户ID") + private Integer userId; + + @Schema(description = "租户id") + private Integer tenantId; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "修改时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + +} diff --git a/src/main/java/com/gxwebsoft/app/entity/AppUser.java b/src/main/java/com/gxwebsoft/app/entity/AppUser.java new file mode 100644 index 0000000..a5d3ad3 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/entity/AppUser.java @@ -0,0 +1,67 @@ +package com.gxwebsoft.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import java.time.LocalDateTime; +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应用成员 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(name = "AppUser对象", description = "应用成员") +public class AppUser implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "自增ID") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @Schema(description = "关联应用ID") + private Long websiteId; + + @Schema(description = "用户ID") + private Integer userId; + + @Schema(description = "用户名(冗余)") + private String username; + + @Schema(description = "头像(冗余)") + private String avatar; + + @Schema(description = "角色: owner/admin/developer/viewer") + private String role; + + @Schema(description = "邀请人用户ID") + private Long inviteBy; + + @Schema(description = "加入时间") + private LocalDateTime inviteTime; + + @Schema(description = "排序(数字越小越靠前)") + private Integer sortNumber; + + @Schema(description = "状态, 0正常, 1冻结") + private Integer status; + + @Schema(description = "租户id") + private Integer tenantId; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "修改时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + +} diff --git a/src/main/java/com/gxwebsoft/app/entity/AppVersion.java b/src/main/java/com/gxwebsoft/app/entity/AppVersion.java new file mode 100644 index 0000000..6442e8b --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/entity/AppVersion.java @@ -0,0 +1,85 @@ +package com.gxwebsoft.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import java.time.LocalDateTime; +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应用版本发布记录 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(name = "AppVersion对象", description = "应用版本发布记录") +public class AppVersion implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "自增ID") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @Schema(description = "关联应用ID") + private Long websiteId; + + @Schema(description = "版本号,如 1.0.0") + private String versionNo; + + @Schema(description = "版本名称") + private String versionName; + + @Schema(description = "版本更新说明") + private String changelog; + + @Schema(description = "安装包地址") + private String packageUrl; + + @Schema(description = "包大小(字节)") + private Long packageSize; + + @Schema(description = "包MD5/SHA256") + private String packageHash; + + @Schema(description = "环境: development/staging/production") + private String env; + + @Schema(description = "状态 0=构建中 1=已发布 2=已回滚 3=构建失败") + private Integer status; + + @Schema(description = "是否为当前版本") + private Boolean isCurrent; + + @Schema(description = "发布人用户ID") + private Long publishBy; + + @Schema(description = "发布时间") + private LocalDateTime publishTime; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序(数字越小越靠前)") + private Integer sortNumber; + + @Schema(description = "用户ID") + private Integer userId; + + @Schema(description = "租户id") + private Integer tenantId; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "修改时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/AppCredentialMapper.java b/src/main/java/com/gxwebsoft/app/mapper/AppCredentialMapper.java new file mode 100644 index 0000000..4ab3599 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/AppCredentialMapper.java @@ -0,0 +1,37 @@ +package com.gxwebsoft.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.app.entity.AppCredential; +import com.gxwebsoft.app.param.AppCredentialParam; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 应用密钥凭证Mapper + * + * @author 科技小王子 + * @since 2026-03-28 21:29:43 + */ +public interface AppCredentialMapper extends BaseMapper { + + /** + * 分页查询 + * + * @param page 分页对象 + * @param param 查询参数 + * @return List + */ + List selectPageRel(@Param("page") IPage page, + @Param("param") AppCredentialParam param); + + /** + * 查询全部 + * + * @param param 查询参数 + * @return List + */ + List selectListRel(@Param("param") AppCredentialParam param); + +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/AppEventMapper.java b/src/main/java/com/gxwebsoft/app/mapper/AppEventMapper.java new file mode 100644 index 0000000..c1fe3ca --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/AppEventMapper.java @@ -0,0 +1,37 @@ +package com.gxwebsoft.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.app.entity.AppEvent; +import com.gxwebsoft.app.param.AppEventParam; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 应用操作动态Mapper + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +public interface AppEventMapper extends BaseMapper { + + /** + * 分页查询 + * + * @param page 分页对象 + * @param param 查询参数 + * @return List + */ + List selectPageRel(@Param("page") IPage page, + @Param("param") AppEventParam param); + + /** + * 查询全部 + * + * @param param 查询参数 + * @return List + */ + List selectListRel(@Param("param") AppEventParam param); + +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/AppUserMapper.java b/src/main/java/com/gxwebsoft/app/mapper/AppUserMapper.java new file mode 100644 index 0000000..a2cca59 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/AppUserMapper.java @@ -0,0 +1,37 @@ +package com.gxwebsoft.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.app.entity.AppUser; +import com.gxwebsoft.app.param.AppUserParam; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 应用成员Mapper + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +public interface AppUserMapper extends BaseMapper { + + /** + * 分页查询 + * + * @param page 分页对象 + * @param param 查询参数 + * @return List + */ + List selectPageRel(@Param("page") IPage page, + @Param("param") AppUserParam param); + + /** + * 查询全部 + * + * @param param 查询参数 + * @return List + */ + List selectListRel(@Param("param") AppUserParam param); + +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/AppVersionMapper.java b/src/main/java/com/gxwebsoft/app/mapper/AppVersionMapper.java new file mode 100644 index 0000000..187b1a6 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/AppVersionMapper.java @@ -0,0 +1,37 @@ +package com.gxwebsoft.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.app.entity.AppVersion; +import com.gxwebsoft.app.param.AppVersionParam; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 应用版本发布记录Mapper + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +public interface AppVersionMapper extends BaseMapper { + + /** + * 分页查询 + * + * @param page 分页对象 + * @param param 查询参数 + * @return List + */ + List selectPageRel(@Param("page") IPage page, + @Param("param") AppVersionParam param); + + /** + * 查询全部 + * + * @param param 查询参数 + * @return List + */ + List selectListRel(@Param("param") AppVersionParam param); + +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppCredentialMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppCredentialMapper.xml new file mode 100644 index 0000000..0c2c0c0 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppCredentialMapper.xml @@ -0,0 +1,78 @@ + + + + + + + SELECT a.* + FROM app_credential a + + + AND a.id = #{param.id} + + + AND a.website_id LIKE CONCAT('%', #{param.websiteId}, '%') + + + AND a.name LIKE CONCAT('%', #{param.name}, '%') + + + AND a.app_id LIKE CONCAT('%', #{param.appId}, '%') + + + AND a.app_secret LIKE CONCAT('%', #{param.appSecret}, '%') + + + AND a.type LIKE CONCAT('%', #{param.type}, '%') + + + AND a.scopes LIKE CONCAT('%', #{param.scopes}, '%') + + + AND a.expire_time LIKE CONCAT('%', #{param.expireTime}, '%') + + + AND a.last_used_at LIKE CONCAT('%', #{param.lastUsedAt}, '%') + + + AND a.remark LIKE CONCAT('%', #{param.remark}, '%') + + + AND a.sort_number = #{param.sortNumber} + + + AND a.status = #{param.status} + + + AND a.deleted = #{param.deleted} + + + AND a.deleted = 0 + + + AND a.user_id = #{param.userId} + + + AND a.create_time >= #{param.createTimeStart} + + + AND a.create_time <= #{param.createTimeEnd} + + + AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') + ) + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppEventMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppEventMapper.xml new file mode 100644 index 0000000..ef63c66 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppEventMapper.xml @@ -0,0 +1,72 @@ + + + + + + + SELECT a.* + FROM app_event a + + + AND a.id = #{param.id} + + + AND a.website_id LIKE CONCAT('%', #{param.websiteId}, '%') + + + AND a.event_type LIKE CONCAT('%', #{param.eventType}, '%') + + + AND a.title LIKE CONCAT('%', #{param.title}, '%') + + + AND a.content LIKE CONCAT('%', #{param.content}, '%') + + + AND a.operator_id LIKE CONCAT('%', #{param.operatorId}, '%') + + + AND a.operator LIKE CONCAT('%', #{param.operator}, '%') + + + AND a.ref_id LIKE CONCAT('%', #{param.refId}, '%') + + + AND a.ref_type LIKE CONCAT('%', #{param.refType}, '%') + + + AND a.extra LIKE CONCAT('%', #{param.extra}, '%') + + + AND a.sort_number = #{param.sortNumber} + + + AND a.status = #{param.status} + + + AND a.user_id = #{param.userId} + + + AND a.create_time >= #{param.createTimeStart} + + + AND a.create_time <= #{param.createTimeEnd} + + + AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') + ) + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml new file mode 100644 index 0000000..e52506a --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml @@ -0,0 +1,63 @@ + + + + + + + SELECT a.* + FROM app_user a + + + AND a.id = #{param.id} + + + AND a.website_id LIKE CONCAT('%', #{param.websiteId}, '%') + + + AND a.user_id LIKE CONCAT('%', #{param.userId}, '%') + + + AND a.username LIKE CONCAT('%', #{param.username}, '%') + + + AND a.avatar LIKE CONCAT('%', #{param.avatar}, '%') + + + AND a.role LIKE CONCAT('%', #{param.role}, '%') + + + AND a.invite_by LIKE CONCAT('%', #{param.inviteBy}, '%') + + + AND a.invite_time LIKE CONCAT('%', #{param.inviteTime}, '%') + + + AND a.sort_number = #{param.sortNumber} + + + AND a.status = #{param.status} + + + AND a.create_time >= #{param.createTimeStart} + + + AND a.create_time <= #{param.createTimeEnd} + + + AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') + ) + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppVersionMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppVersionMapper.xml new file mode 100644 index 0000000..23dc680 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppVersionMapper.xml @@ -0,0 +1,81 @@ + + + + + + + SELECT a.* + FROM app_version a + + + AND a.id = #{param.id} + + + AND a.website_id LIKE CONCAT('%', #{param.websiteId}, '%') + + + AND a.version_no LIKE CONCAT('%', #{param.versionNo}, '%') + + + AND a.version_name LIKE CONCAT('%', #{param.versionName}, '%') + + + AND a.changelog LIKE CONCAT('%', #{param.changelog}, '%') + + + AND a.package_url LIKE CONCAT('%', #{param.packageUrl}, '%') + + + AND a.package_size LIKE CONCAT('%', #{param.packageSize}, '%') + + + AND a.package_hash LIKE CONCAT('%', #{param.packageHash}, '%') + + + AND a.env LIKE CONCAT('%', #{param.env}, '%') + + + AND a.status = #{param.status} + + + AND a.is_current = #{param.isCurrent} + + + AND a.publish_by LIKE CONCAT('%', #{param.publishBy}, '%') + + + AND a.publish_time LIKE CONCAT('%', #{param.publishTime}, '%') + + + AND a.remark LIKE CONCAT('%', #{param.remark}, '%') + + + AND a.sort_number = #{param.sortNumber} + + + AND a.user_id = #{param.userId} + + + AND a.create_time >= #{param.createTimeStart} + + + AND a.create_time <= #{param.createTimeEnd} + + + AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') + ) + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/app/param/AppCredentialParam.java b/src/main/java/com/gxwebsoft/app/param/AppCredentialParam.java new file mode 100644 index 0000000..cd53d0b --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/param/AppCredentialParam.java @@ -0,0 +1,71 @@ +package com.gxwebsoft.app.param; + +import java.math.BigDecimal; +import com.gxwebsoft.common.core.annotation.QueryField; +import com.gxwebsoft.common.core.annotation.QueryType; +import com.gxwebsoft.common.core.web.BaseParam; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应用密钥凭证查询参数 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:43 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(name = "AppCredentialParam对象", description = "应用密钥凭证查询参数") +public class AppCredentialParam extends BaseParam { + private static final long serialVersionUID = 1L; + + @Schema(description = "自增ID") + @QueryField(type = QueryType.EQ) + private Long id; + + @Schema(description = "关联应用ID") + private Long websiteId; + + @Schema(description = "凭证名称,如生产环境密钥") + private String name; + + @Schema(description = "App ID(公开)") + private String appId; + + @Schema(description = "App Secret(加密存储)") + private String appSecret; + + @Schema(description = "凭证类型: server/client/webhook") + private String type; + + @Schema(description = "权限范围,空格分隔") + private String scopes; + + @Schema(description = "到期时间,NULL=永不过期") + private String expireTime; + + @Schema(description = "最后使用时间") + private String lastUsedAt; + + private String remark; + + @Schema(description = "排序(数字越小越靠前)") + @QueryField(type = QueryType.EQ) + private Integer sortNumber; + + @Schema(description = "状态, 0正常, 1冻结") + @QueryField(type = QueryType.EQ) + private Integer status; + + @Schema(description = "是否删除, 0否, 1是") + @QueryField(type = QueryType.EQ) + private Integer deleted; + + @Schema(description = "用户ID") + @QueryField(type = QueryType.EQ) + private Integer userId; + +} diff --git a/src/main/java/com/gxwebsoft/app/param/AppEventParam.java b/src/main/java/com/gxwebsoft/app/param/AppEventParam.java new file mode 100644 index 0000000..64b2e2a --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/param/AppEventParam.java @@ -0,0 +1,68 @@ +package com.gxwebsoft.app.param; + +import java.math.BigDecimal; +import com.gxwebsoft.common.core.annotation.QueryField; +import com.gxwebsoft.common.core.annotation.QueryType; +import com.gxwebsoft.common.core.web.BaseParam; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应用操作动态查询参数 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(name = "AppEventParam对象", description = "应用操作动态查询参数") +public class AppEventParam extends BaseParam { + private static final long serialVersionUID = 1L; + + @Schema(description = "自增ID") + @QueryField(type = QueryType.EQ) + private Long id; + + @Schema(description = "关联应用ID") + private Long websiteId; + + @Schema(description = "事件类型: created/published/updated/domain_bound/member_added/status_changed") + private String eventType; + + @Schema(description = "事件标题,如已发布") + private String title; + + @Schema(description = "详细描述") + private String content; + + @Schema(description = "操作人用户ID") + private Long operatorId; + + @Schema(description = "操作人名称(冗余)") + private String operator; + + @Schema(description = "关联ID,如版本ID") + private Long refId; + + @Schema(description = "关联类型") + private String refType; + + @Schema(description = "扩展数据") + private String extra; + + @Schema(description = "排序(数字越小越靠前)") + @QueryField(type = QueryType.EQ) + private Integer sortNumber; + + @Schema(description = "状态, 0正常, 1冻结") + @QueryField(type = QueryType.EQ) + private Integer status; + + @Schema(description = "用户ID") + @QueryField(type = QueryType.EQ) + private Integer userId; + +} diff --git a/src/main/java/com/gxwebsoft/app/param/AppUserParam.java b/src/main/java/com/gxwebsoft/app/param/AppUserParam.java new file mode 100644 index 0000000..d9578f0 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/param/AppUserParam.java @@ -0,0 +1,58 @@ +package com.gxwebsoft.app.param; + +import java.math.BigDecimal; +import com.gxwebsoft.common.core.annotation.QueryField; +import com.gxwebsoft.common.core.annotation.QueryType; +import com.gxwebsoft.common.core.web.BaseParam; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应用成员查询参数 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(name = "AppUserParam对象", description = "应用成员查询参数") +public class AppUserParam extends BaseParam { + private static final long serialVersionUID = 1L; + + @Schema(description = "自增ID") + @QueryField(type = QueryType.EQ) + private Long id; + + @Schema(description = "关联应用ID") + private Long websiteId; + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "用户名(冗余)") + private String username; + + @Schema(description = "头像(冗余)") + private String avatar; + + @Schema(description = "角色: owner/admin/developer/viewer") + private String role; + + @Schema(description = "邀请人用户ID") + private Long inviteBy; + + @Schema(description = "加入时间") + private String inviteTime; + + @Schema(description = "排序(数字越小越靠前)") + @QueryField(type = QueryType.EQ) + private Integer sortNumber; + + @Schema(description = "状态, 0正常, 1冻结") + @QueryField(type = QueryType.EQ) + private Integer status; + +} diff --git a/src/main/java/com/gxwebsoft/app/param/AppVersionParam.java b/src/main/java/com/gxwebsoft/app/param/AppVersionParam.java new file mode 100644 index 0000000..a986bc0 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/param/AppVersionParam.java @@ -0,0 +1,78 @@ +package com.gxwebsoft.app.param; + +import java.math.BigDecimal; +import com.gxwebsoft.common.core.annotation.QueryField; +import com.gxwebsoft.common.core.annotation.QueryType; +import com.gxwebsoft.common.core.web.BaseParam; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应用版本发布记录查询参数 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(name = "AppVersionParam对象", description = "应用版本发布记录查询参数") +public class AppVersionParam extends BaseParam { + private static final long serialVersionUID = 1L; + + @Schema(description = "自增ID") + @QueryField(type = QueryType.EQ) + private Long id; + + @Schema(description = "关联应用ID") + private Long websiteId; + + @Schema(description = "版本号,如 1.0.0") + private String versionNo; + + @Schema(description = "版本名称") + private String versionName; + + @Schema(description = "版本更新说明") + private String changelog; + + @Schema(description = "安装包地址") + private String packageUrl; + + @Schema(description = "包大小(字节)") + private Long packageSize; + + @Schema(description = "包MD5/SHA256") + private String packageHash; + + @Schema(description = "环境: development/staging/production") + private String env; + + @Schema(description = "状态 0=构建中 1=已发布 2=已回滚 3=构建失败") + @QueryField(type = QueryType.EQ) + private Integer status; + + @Schema(description = "是否为当前版本") + @QueryField(type = QueryType.EQ) + private Boolean isCurrent; + + @Schema(description = "发布人用户ID") + private Long publishBy; + + @Schema(description = "发布时间") + private String publishTime; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序(数字越小越靠前)") + @QueryField(type = QueryType.EQ) + private Integer sortNumber; + + @Schema(description = "用户ID") + @QueryField(type = QueryType.EQ) + private Integer userId; + +} diff --git a/src/main/java/com/gxwebsoft/app/service/AppCredentialService.java b/src/main/java/com/gxwebsoft/app/service/AppCredentialService.java new file mode 100644 index 0000000..6a426ce --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/AppCredentialService.java @@ -0,0 +1,42 @@ +package com.gxwebsoft.app.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.app.entity.AppCredential; +import com.gxwebsoft.app.param.AppCredentialParam; + +import java.util.List; + +/** + * 应用密钥凭证Service + * + * @author 科技小王子 + * @since 2026-03-28 21:29:43 + */ +public interface AppCredentialService extends IService { + + /** + * 分页关联查询 + * + * @param param 查询参数 + * @return PageResult + */ + PageResult pageRel(AppCredentialParam param); + + /** + * 关联查询全部 + * + * @param param 查询参数 + * @return List + */ + List listRel(AppCredentialParam param); + + /** + * 根据id查询 + * + * @param id 自增ID + * @return AppCredential + */ + AppCredential getByIdRel(Integer id); + +} diff --git a/src/main/java/com/gxwebsoft/app/service/AppEventService.java b/src/main/java/com/gxwebsoft/app/service/AppEventService.java new file mode 100644 index 0000000..34d8d89 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/AppEventService.java @@ -0,0 +1,42 @@ +package com.gxwebsoft.app.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.app.entity.AppEvent; +import com.gxwebsoft.app.param.AppEventParam; + +import java.util.List; + +/** + * 应用操作动态Service + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +public interface AppEventService extends IService { + + /** + * 分页关联查询 + * + * @param param 查询参数 + * @return PageResult + */ + PageResult pageRel(AppEventParam param); + + /** + * 关联查询全部 + * + * @param param 查询参数 + * @return List + */ + List listRel(AppEventParam param); + + /** + * 根据id查询 + * + * @param id 自增ID + * @return AppEvent + */ + AppEvent getByIdRel(Integer id); + +} diff --git a/src/main/java/com/gxwebsoft/app/service/AppUserService.java b/src/main/java/com/gxwebsoft/app/service/AppUserService.java new file mode 100644 index 0000000..82706e9 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/AppUserService.java @@ -0,0 +1,42 @@ +package com.gxwebsoft.app.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.app.entity.AppUser; +import com.gxwebsoft.app.param.AppUserParam; + +import java.util.List; + +/** + * 应用成员Service + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +public interface AppUserService extends IService { + + /** + * 分页关联查询 + * + * @param param 查询参数 + * @return PageResult + */ + PageResult pageRel(AppUserParam param); + + /** + * 关联查询全部 + * + * @param param 查询参数 + * @return List + */ + List listRel(AppUserParam param); + + /** + * 根据id查询 + * + * @param id 自增ID + * @return AppUser + */ + AppUser getByIdRel(Integer id); + +} diff --git a/src/main/java/com/gxwebsoft/app/service/AppVersionService.java b/src/main/java/com/gxwebsoft/app/service/AppVersionService.java new file mode 100644 index 0000000..8a56e0e --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/AppVersionService.java @@ -0,0 +1,42 @@ +package com.gxwebsoft.app.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.app.entity.AppVersion; +import com.gxwebsoft.app.param.AppVersionParam; + +import java.util.List; + +/** + * 应用版本发布记录Service + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +public interface AppVersionService extends IService { + + /** + * 分页关联查询 + * + * @param param 查询参数 + * @return PageResult + */ + PageResult pageRel(AppVersionParam param); + + /** + * 关联查询全部 + * + * @param param 查询参数 + * @return List + */ + List listRel(AppVersionParam param); + + /** + * 根据id查询 + * + * @param id 自增ID + * @return AppVersion + */ + AppVersion getByIdRel(Integer id); + +} diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppCredentialServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppCredentialServiceImpl.java new file mode 100644 index 0000000..3028fde --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppCredentialServiceImpl.java @@ -0,0 +1,47 @@ +package com.gxwebsoft.app.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.app.mapper.AppCredentialMapper; +import com.gxwebsoft.app.service.AppCredentialService; +import com.gxwebsoft.app.entity.AppCredential; +import com.gxwebsoft.app.param.AppCredentialParam; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 应用密钥凭证Service实现 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:43 + */ +@Service +public class AppCredentialServiceImpl extends ServiceImpl implements AppCredentialService { + + @Override + public PageResult pageRel(AppCredentialParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("sort_number asc, create_time desc"); + List list = baseMapper.selectPageRel(page, param); + return new PageResult<>(list, page.getTotal()); + } + + @Override + public List listRel(AppCredentialParam param) { + List list = baseMapper.selectListRel(param); + // 排序 + PageParam page = new PageParam<>(); + page.setDefaultOrder("sort_number asc, create_time desc"); + return page.sortRecords(list); + } + + @Override + public AppCredential getByIdRel(Integer id) { + AppCredentialParam param = new AppCredentialParam(); + param.setId(id); + return param.getOne(baseMapper.selectListRel(param)); + } + +} diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppEventServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppEventServiceImpl.java new file mode 100644 index 0000000..91c147a --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppEventServiceImpl.java @@ -0,0 +1,47 @@ +package com.gxwebsoft.app.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.app.mapper.AppEventMapper; +import com.gxwebsoft.app.service.AppEventService; +import com.gxwebsoft.app.entity.AppEvent; +import com.gxwebsoft.app.param.AppEventParam; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 应用操作动态Service实现 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Service +public class AppEventServiceImpl extends ServiceImpl implements AppEventService { + + @Override + public PageResult pageRel(AppEventParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("sort_number asc, create_time desc"); + List list = baseMapper.selectPageRel(page, param); + return new PageResult<>(list, page.getTotal()); + } + + @Override + public List listRel(AppEventParam param) { + List list = baseMapper.selectListRel(param); + // 排序 + PageParam page = new PageParam<>(); + page.setDefaultOrder("sort_number asc, create_time desc"); + return page.sortRecords(list); + } + + @Override + public AppEvent getByIdRel(Integer id) { + AppEventParam param = new AppEventParam(); + param.setId(id); + return param.getOne(baseMapper.selectListRel(param)); + } + +} diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java new file mode 100644 index 0000000..2f2fb01 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java @@ -0,0 +1,47 @@ +package com.gxwebsoft.app.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.app.mapper.AppUserMapper; +import com.gxwebsoft.app.service.AppUserService; +import com.gxwebsoft.app.entity.AppUser; +import com.gxwebsoft.app.param.AppUserParam; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 应用成员Service实现 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Service +public class AppUserServiceImpl extends ServiceImpl implements AppUserService { + + @Override + public PageResult pageRel(AppUserParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("sort_number asc, create_time desc"); + List list = baseMapper.selectPageRel(page, param); + return new PageResult<>(list, page.getTotal()); + } + + @Override + public List listRel(AppUserParam param) { + List list = baseMapper.selectListRel(param); + // 排序 + PageParam page = new PageParam<>(); + page.setDefaultOrder("sort_number asc, create_time desc"); + return page.sortRecords(list); + } + + @Override + public AppUser getByIdRel(Integer id) { + AppUserParam param = new AppUserParam(); + param.setId(id); + return param.getOne(baseMapper.selectListRel(param)); + } + +} diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppVersionServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppVersionServiceImpl.java new file mode 100644 index 0000000..3a5e90b --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppVersionServiceImpl.java @@ -0,0 +1,47 @@ +package com.gxwebsoft.app.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.app.mapper.AppVersionMapper; +import com.gxwebsoft.app.service.AppVersionService; +import com.gxwebsoft.app.entity.AppVersion; +import com.gxwebsoft.app.param.AppVersionParam; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 应用版本发布记录Service实现 + * + * @author 科技小王子 + * @since 2026-03-28 21:29:44 + */ +@Service +public class AppVersionServiceImpl extends ServiceImpl implements AppVersionService { + + @Override + public PageResult pageRel(AppVersionParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("sort_number asc, create_time desc"); + List list = baseMapper.selectPageRel(page, param); + return new PageResult<>(list, page.getTotal()); + } + + @Override + public List listRel(AppVersionParam param) { + List list = baseMapper.selectListRel(param); + // 排序 + PageParam page = new PageParam<>(); + page.setDefaultOrder("sort_number asc, create_time desc"); + return page.sortRecords(list); + } + + @Override + public AppVersion getByIdRel(Integer id) { + AppVersionParam param = new AppVersionParam(); + param.setId(id); + return param.getOne(baseMapper.selectListRel(param)); + } + +} diff --git a/src/test/java/com/gxwebsoft/generator/AppGenerator.java b/src/test/java/com/gxwebsoft/generator/AppGenerator.java new file mode 100644 index 0000000..5557843 --- /dev/null +++ b/src/test/java/com/gxwebsoft/generator/AppGenerator.java @@ -0,0 +1,384 @@ +package com.gxwebsoft.generator; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.generator.AutoGenerator; +import com.baomidou.mybatisplus.generator.InjectionConfig; +import com.baomidou.mybatisplus.generator.config.*; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; +import com.gxwebsoft.generator.engine.BeetlTemplateEnginePlus; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * CMS模块-代码生成工具 + * + * @author WebSoft + * @since 2021-09-05 00:31:14 + */ +public class AppGenerator { + // 输出位置 + private static final String OUTPUT_LOCATION = System.getProperty("user.dir"); + //private static final String OUTPUT_LOCATION = "D:/codegen"; // 不想生成到项目中可以写磁盘路径 + // JAVA输出目录 + private static final String OUTPUT_DIR = "/src/main/java"; + // Vue文件输出位置 + private static final String OUTPUT_LOCATION_VUE = "/Users/gxwebsoft/VUE/mp-vue"; + // UniApp文件输出目录 + private static final String OUTPUT_LOCATION_UNIAPP = "/Users/gxwebsoft/VUE/template-5"; + // Vue文件输出目录 + private static final String OUTPUT_DIR_VUE = "/src"; + // 作者名称 + private static final String AUTHOR = "科技小王子"; + // 是否在xml中添加二级缓存配置 + private static final boolean ENABLE_CACHE = false; + // 数据库连接配置 + private static final String DB_URL = "jdbc:mysql://47.119.165.234:13308/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8"; + private static final String DB_DRIVER = "com.mysql.cj.jdbc.Driver"; + private static final String DB_USERNAME = "modules"; + private static final String DB_PASSWORD = "P7KsAyDXG8YdLnkA"; + // 包名 + private static final String PACKAGE_NAME = "com.gxwebsoft"; + // 模块名 + private static final String MODULE_NAME = "app"; + // 需要生成的表 + private static final String[] TABLE_NAMES = new String[]{ + "app_user", + "app_version", + "app_event", + "app_credential" + }; + // 需要去除的表前缀 + private static final String[] TABLE_PREFIX = new String[]{ + "tb_" + }; + // 不需要作为查询参数的字段 + private static final String[] PARAM_EXCLUDE_FIELDS = new String[]{ + "tenant_id", + "create_time", + "update_time" + }; + // 查询参数使用String的类型 + private static final String[] PARAM_TO_STRING_TYPE = new String[]{ + "Date", + "LocalDate", + "LocalTime", + "LocalDateTime" + }; + // 查询参数使用EQ的类型 + private static final String[] PARAM_EQ_TYPE = new String[]{ + "Integer", + "Boolean", + "BigDecimal" + }; + // 是否添加权限注解 + private static final boolean AUTH_ANNOTATION = true; + // 是否添加日志注解 + private static final boolean LOG_ANNOTATION = true; + // controller的mapping前缀 + private static final String CONTROLLER_MAPPING_PREFIX = "/api"; + // 模板所在位置 + private static final String TEMPLATES_DIR = "/src/test/java/com/gxwebsoft/generator/templates"; + + public static void main(String[] args) { + // 代码生成器 + AutoGenerator mpg = new AutoGenerator(); + + // 全局配置 + GlobalConfig gc = new GlobalConfig(); + gc.setOutputDir(OUTPUT_LOCATION + OUTPUT_DIR); + gc.setAuthor(AUTHOR); + gc.setOpen(false); + gc.setFileOverride(true); + gc.setEnableCache(ENABLE_CACHE); + gc.setSwagger2(true); + gc.setIdType(IdType.AUTO); + gc.setServiceName("%sService"); + mpg.setGlobalConfig(gc); + + // 数据源配置 + DataSourceConfig dsc = new DataSourceConfig(); + dsc.setUrl(DB_URL); + // dsc.setSchemaName("public"); + dsc.setDriverName(DB_DRIVER); + dsc.setUsername(DB_USERNAME); + dsc.setPassword(DB_PASSWORD); + mpg.setDataSource(dsc); + + // 包配置 + PackageConfig pc = new PackageConfig(); + pc.setModuleName(MODULE_NAME); + pc.setParent(PACKAGE_NAME); + mpg.setPackageInfo(pc); + + // 策略配置 + StrategyConfig strategy = new StrategyConfig(); + strategy.setNaming(NamingStrategy.underline_to_camel); + strategy.setColumnNaming(NamingStrategy.underline_to_camel); + strategy.setInclude(TABLE_NAMES); + strategy.setTablePrefix(TABLE_PREFIX); + strategy.setSuperControllerClass(PACKAGE_NAME + ".common.core.web.BaseController"); + strategy.setEntityLombokModel(true); + strategy.setRestControllerStyle(true); + strategy.setControllerMappingHyphenStyle(true); + strategy.setLogicDeleteFieldName("deleted"); + mpg.setStrategy(strategy); + + // 模板配置 + TemplateConfig templateConfig = new TemplateConfig(); + templateConfig.setController(TEMPLATES_DIR + "/controller.java"); + templateConfig.setEntity(TEMPLATES_DIR + "/entity.java"); + templateConfig.setMapper(TEMPLATES_DIR + "/mapper.java"); + templateConfig.setXml(TEMPLATES_DIR + "/mapper.xml"); + templateConfig.setService(TEMPLATES_DIR + "/service.java"); + templateConfig.setServiceImpl(TEMPLATES_DIR + "/serviceImpl.java"); + mpg.setTemplate(templateConfig); + mpg.setTemplateEngine(new BeetlTemplateEnginePlus()); + + // 自定义模板配置 + InjectionConfig cfg = new InjectionConfig() { + @Override + public void initMap() { + Map map = new HashMap<>(); + map.put("packageName", PACKAGE_NAME); + map.put("paramExcludeFields", PARAM_EXCLUDE_FIELDS); + map.put("paramToStringType", PARAM_TO_STRING_TYPE); + map.put("paramEqType", PARAM_EQ_TYPE); + map.put("authAnnotation", AUTH_ANNOTATION); + map.put("logAnnotation", LOG_ANNOTATION); + map.put("controllerMappingPrefix", CONTROLLER_MAPPING_PREFIX); + // 添加项目类型标识,用于模板中的条件判断 + map.put("isUniApp", false); // Vue 项目 + map.put("isVueAdmin", true); // 后台管理项目 + this.setMap(map); + } + }; + String templatePath = TEMPLATES_DIR + "/param.java.btl"; + List focList = new ArrayList<>(); + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION + OUTPUT_DIR + "/" + + PACKAGE_NAME.replace(".", "/") + + "/" + pc.getModuleName() + "/param/" + + tableInfo.getEntityName() + "Param" + StringPool.DOT_JAVA; + } + }); + /** + * 以下是生成VUE项目代码 + * 生成文件的路径 /api/shop/goods/index.ts + */ + templatePath = TEMPLATES_DIR + "/index.ts.btl"; + + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_VUE + OUTPUT_DIR_VUE + + "/api/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/" + "index.ts"; + } + }); + // UniApp 使用专门的模板 + String uniappTemplatePath = TEMPLATES_DIR + "/index.ts.uniapp.btl"; + focList.add(new FileOutConfig(uniappTemplatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE + + "/api/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/" + "index.ts"; + } + }); + // 生成TS文件 (/api/shop/goods/model/index.ts) + templatePath = TEMPLATES_DIR + "/model.ts.btl"; + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_VUE + OUTPUT_DIR_VUE + + "/api/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/model/" + "index.ts"; + } + }); + // UniApp 使用专门的 model 模板 + String uniappModelTemplatePath = TEMPLATES_DIR + "/model.ts.uniapp.btl"; + focList.add(new FileOutConfig(uniappModelTemplatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE + + "/api/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/model/" + "index.ts"; + } + }); + // 生成Vue文件(/views/shop/goods/index.vue) + templatePath = TEMPLATES_DIR + "/index.vue.btl"; + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_VUE + OUTPUT_DIR_VUE + + "/views/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/" + "index.vue"; + } + }); + + // 生成components文件(/views/shop/goods/components/edit.vue) + templatePath = TEMPLATES_DIR + "/components.edit.vue.btl"; + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_VUE + OUTPUT_DIR_VUE + + "/views/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/components/" + tableInfo.getEntityPath() + "Edit.vue"; + } + }); + + // 生成components文件(/views/shop/goods/components/search.vue) + templatePath = TEMPLATES_DIR + "/components.search.vue.btl"; + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_VUE + OUTPUT_DIR_VUE + + "/views/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/components/" + "search.vue"; + } + }); + + // ========== 移动端页面文件生成 ========== + // 生成移动端列表页面配置文件 (/src/shop/goods/index.config.ts) + templatePath = TEMPLATES_DIR + "/index.config.ts.btl"; + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE + + "/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/" + "index.config.ts"; + } + }); + + // 生成移动端列表页面组件文件 (/src/shop/goods/index.tsx) + templatePath = TEMPLATES_DIR + "/index.tsx.btl"; + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE + + "/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/" + "index.tsx"; + } + }); + + // 生成移动端新增/编辑页面配置文件 (/src/shop/goods/add.config.ts) + templatePath = TEMPLATES_DIR + "/add.config.ts.btl"; + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE + + "/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/" + "add.config.ts"; + } + }); + + // 生成移动端新增/编辑页面组件文件 (/src/shop/goods/add.tsx) + templatePath = TEMPLATES_DIR + "/add.tsx.btl"; + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE + + "/" + pc.getModuleName() + "/" + + tableInfo.getEntityPath() + "/" + "add.tsx"; + } + }); + + cfg.setFileOutConfigList(focList); + mpg.setCfg(cfg); + + mpg.execute(); + + // 自动更新 app.config.ts + updateAppConfig(TABLE_NAMES, MODULE_NAME); + } + + /** + * 自动更新 app.config.ts 文件,添加新生成的页面路径 + */ + private static void updateAppConfig(String[] tableNames, String moduleName) { + String appConfigPath = OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE + "/app.config.ts"; + + try { + // 读取原文件内容 + String content = new String(Files.readAllBytes(Paths.get(appConfigPath))); + + // 为每个表生成页面路径 + StringBuilder newPages = new StringBuilder(); + for (String tableName : tableNames) { + String entityPath = tableName.replaceAll("_", ""); + // 转换为驼峰命名 + String[] parts = tableName.split("_"); + StringBuilder camelCase = new StringBuilder(parts[0]); + for (int i = 1; i < parts.length; i++) { + camelCase.append(parts[i].substring(0, 1).toUpperCase()).append(parts[i].substring(1)); + } + entityPath = camelCase.toString(); + + newPages.append(" '").append(entityPath).append("/index',\n"); + newPages.append(" '").append(entityPath).append("/add',\n"); + } + + // 查找对应模块的子包配置 + String modulePattern = "\"root\":\\s*\"" + moduleName + "\",\\s*\"pages\":\\s*\\[([^\\]]*)]"; + Pattern pattern = Pattern.compile(modulePattern, Pattern.DOTALL); + Matcher matcher = pattern.matcher(content); + + if (matcher.find()) { + String existingPages = matcher.group(1); + + // 检查页面是否已存在,避免重复添加 + boolean needUpdate = false; + String[] newPageArray = newPages.toString().split("\n"); + for (String newPage : newPageArray) { + if (!newPage.trim().isEmpty() && !existingPages.contains(newPage.trim().replace(" ", "").replace(",", ""))) { + needUpdate = true; + break; + } + } + + if (needUpdate) { + // 备份原文件 + String backupPath = appConfigPath + ".backup." + System.currentTimeMillis(); + Files.copy(Paths.get(appConfigPath), Paths.get(backupPath)); + System.out.println("已备份原文件到: " + backupPath); + + // 在现有页面列表末尾添加新页面 + String updatedPages = existingPages.trim(); + if (!updatedPages.endsWith(",")) { + updatedPages += ","; + } + updatedPages += "\n" + newPages.toString().trim(); + + // 替换内容 + String updatedContent = content.replace(matcher.group(1), updatedPages); + + // 写入更新后的内容 + Files.write(Paths.get(appConfigPath), updatedContent.getBytes()); + + System.out.println("✅ 已自动更新 app.config.ts,添加了以下页面路径:"); + System.out.println(newPages.toString()); + } else { + System.out.println("ℹ️ app.config.ts 中已包含所有页面路径,无需更新"); + } + } else { + System.out.println("⚠️ 未找到 " + moduleName + " 模块的子包配置,请手动添加页面路径"); + } + + } catch (Exception e) { + System.err.println("❌ 更新 app.config.ts 失败: " + e.getMessage()); + e.printStackTrace(); + } + } + +} From 7dede6f36fc0fee85efe85dd96b9c3ce2ad59f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sat, 28 Mar 2026 21:47:32 +0800 Subject: [PATCH 02/21] =?UTF-8?q?feat(app):=20=E5=AE=8C=E5=96=84=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=87=AD=E8=AF=81=E3=80=81=E4=BA=8B=E4=BB=B6=E5=92=8C?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增应用密钥凭证的创建、重置和状态管理功能 - 实现AppSecret自动生成功能并添加脱敏显示机制 - 增加应用操作动态的最新记录查询和批量清理功能 - 添加应用成员邀请和角色修改功能 - 优化查询条件支持精确匹配和租户隔离 - 集成网站信息关联查询并完善数据脱敏处理 --- .../controller/AppCredentialController.java | 56 ++++++++---- .../app/controller/AppEventController.java | 76 ++++++++-------- .../app/controller/AppUserController.java | 65 ++++++++++---- .../app/controller/AppVersionController.java | 88 ++++++++++++++----- .../app/mapper/xml/AppCredentialMapper.xml | 35 +++----- .../app/mapper/xml/AppEventMapper.xml | 42 ++++----- .../app/mapper/xml/AppUserMapper.xml | 36 ++++---- .../app/mapper/xml/AppVersionMapper.xml | 49 ++++------- .../app/param/AppCredentialParam.java | 14 +-- .../gxwebsoft/app/param/AppEventParam.java | 25 +++--- .../com/gxwebsoft/app/param/AppUserParam.java | 19 ++-- .../gxwebsoft/app/param/AppVersionParam.java | 33 ++----- .../app/service/AppCredentialService.java | 33 +++++-- .../app/service/AppEventService.java | 36 ++++++-- .../gxwebsoft/app/service/AppUserService.java | 36 ++++++-- .../app/service/AppVersionService.java | 32 +++++-- .../impl/AppCredentialServiceImpl.java | 86 +++++++++++++++++- .../app/service/impl/AppEventServiceImpl.java | 47 +++++++++- .../app/service/impl/AppUserServiceImpl.java | 50 ++++++++++- .../service/impl/AppVersionServiceImpl.java | 73 ++++++++++++++- 20 files changed, 634 insertions(+), 297 deletions(-) diff --git a/src/main/java/com/gxwebsoft/app/controller/AppCredentialController.java b/src/main/java/com/gxwebsoft/app/controller/AppCredentialController.java index 17e68da..6b9ce15 100644 --- a/src/main/java/com/gxwebsoft/app/controller/AppCredentialController.java +++ b/src/main/java/com/gxwebsoft/app/controller/AppCredentialController.java @@ -6,12 +6,12 @@ import com.gxwebsoft.app.entity.AppCredential; import com.gxwebsoft.app.param.AppCredentialParam; import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.PageResult; -import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.system.entity.User; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -24,64 +24,86 @@ import java.util.List; * @author 科技小王子 * @since 2026-03-28 21:29:44 */ +@Slf4j @Tag(name = "应用密钥凭证管理") @RestController @RequestMapping("/api/app/app-credential") public class AppCredentialController extends BaseController { + @Resource private AppCredentialService appCredentialService; - @PreAuthorize("hasAuthority('app:appCredential:list')") @Operation(summary = "分页查询应用密钥凭证") @GetMapping("/page") public ApiResult> page(AppCredentialParam param) { - // 使用关联查询 return success(appCredentialService.pageRel(param)); } - @PreAuthorize("hasAuthority('app:appCredential:list')") @Operation(summary = "查询全部应用密钥凭证") @GetMapping() public ApiResult> list(AppCredentialParam param) { - // 使用关联查询 return success(appCredentialService.listRel(param)); } - @PreAuthorize("hasAuthority('app:appCredential:list')") @Operation(summary = "根据id查询应用密钥凭证") @GetMapping("/{id}") public ApiResult get(@PathVariable("id") Integer id) { - // 使用关联查询 return success(appCredentialService.getByIdRel(id)); } @PreAuthorize("hasAuthority('app:appCredential:save')") @OperationLog - @Operation(summary = "添加应用密钥凭证") + @Operation(summary = "创建应用密钥凭证(自动生成 AppID 和 AppSecret)") @PostMapping() public ApiResult save(@RequestBody AppCredential appCredential) { - // 记录当前登录用户id User loginUser = getLoginUser(); - if (loginUser != null) { - appCredential.setUserId(loginUser.getUserId()); + if (loginUser == null) { + return fail("请先登录"); } - if (appCredentialService.save(appCredential)) { - return success("添加成功"); - } - return fail("添加失败"); + appCredential.setUserId(loginUser.getUserId()); + // 创建并生成密钥 + AppCredential result = appCredentialService.createCredential(appCredential); + return success("创建成功,请保存 AppSecret,该信息仅展示一次", result); } @PreAuthorize("hasAuthority('app:appCredential:update')") @OperationLog - @Operation(summary = "修改应用密钥凭证") + @Operation(summary = "修改应用密钥凭证(名称/类型/备注等,不含密钥)") @PutMapping() public ApiResult update(@RequestBody AppCredential appCredential) { + // 防止通过此接口直接修改 appId/appSecret + appCredential.setAppId(null); + appCredential.setAppSecret(null); if (appCredentialService.updateById(appCredential)) { return success("修改成功"); } return fail("修改失败"); } + @PreAuthorize("hasAuthority('app:appCredential:update')") + @OperationLog + @Operation(summary = "重置 AppSecret(重新生成密钥)") + @PostMapping("/resetSecret/{id}") + public ApiResult resetSecret(@PathVariable("id") Long id) { + try { + AppCredential result = appCredentialService.resetSecret(id); + return success("重置成功,请保存新 AppSecret,该信息仅展示一次", result); + } catch (RuntimeException e) { + return fail(e.getMessage()); + } + } + + @PreAuthorize("hasAuthority('app:appCredential:update')") + @OperationLog + @Operation(summary = "禁用/启用凭证") + @PutMapping("/status/{id}/{status}") + public ApiResult updateStatus(@PathVariable("id") Long id, @PathVariable("status") Integer status) { + if (appCredentialService.updateStatus(id, status)) { + return success(status == 0 ? "已启用" : "已禁用"); + } + return fail("操作失败"); + } + @PreAuthorize("hasAuthority('app:appCredential:remove')") @OperationLog @Operation(summary = "删除应用密钥凭证") @@ -108,7 +130,7 @@ public class AppCredentialController extends BaseController { @OperationLog @Operation(summary = "批量修改应用密钥凭证") @PutMapping("/batch") - public ApiResult removeBatch(@RequestBody BatchParam batchParam) { + public ApiResult updateBatch(@RequestBody BatchParam batchParam) { if (batchParam.update(appCredentialService, "id")) { return success("修改成功"); } diff --git a/src/main/java/com/gxwebsoft/app/controller/AppEventController.java b/src/main/java/com/gxwebsoft/app/controller/AppEventController.java index 84a7413..e963137 100644 --- a/src/main/java/com/gxwebsoft/app/controller/AppEventController.java +++ b/src/main/java/com/gxwebsoft/app/controller/AppEventController.java @@ -6,12 +6,11 @@ import com.gxwebsoft.app.entity.AppEvent; import com.gxwebsoft.app.param.AppEventParam; import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.PageResult; -import com.gxwebsoft.common.core.web.PageParam; -import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.system.entity.User; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -24,56 +23,64 @@ import java.util.List; * @author 科技小王子 * @since 2026-03-28 21:29:44 */ +@Slf4j @Tag(name = "应用操作动态管理") @RestController @RequestMapping("/api/app/app-event") public class AppEventController extends BaseController { + @Resource private AppEventService appEventService; - @PreAuthorize("hasAuthority('app:appEvent:list')") - @Operation(summary = "分页查询应用操作动态") + @Operation(summary = "分页查询操作动态") @GetMapping("/page") public ApiResult> page(AppEventParam param) { - // 使用关联查询 return success(appEventService.pageRel(param)); } - @PreAuthorize("hasAuthority('app:appEvent:list')") - @Operation(summary = "查询全部应用操作动态") + @Operation(summary = "查询全部操作动态") @GetMapping() public ApiResult> list(AppEventParam param) { - // 使用关联查询 return success(appEventService.listRel(param)); } - @PreAuthorize("hasAuthority('app:appEvent:list')") - @Operation(summary = "根据id查询应用操作动态") + @Operation(summary = "根据id查询操作动态") @GetMapping("/{id}") public ApiResult get(@PathVariable("id") Integer id) { - // 使用关联查询 return success(appEventService.getByIdRel(id)); } + @Operation(summary = "获取应用最新一条动态(用于卡片展示)") + @GetMapping("/latest/{websiteId}") + public ApiResult getLatest(@PathVariable("websiteId") Long websiteId) { + return success(appEventService.getLatestEvent(websiteId)); + } + @PreAuthorize("hasAuthority('app:appEvent:save')") @OperationLog - @Operation(summary = "添加应用操作动态") + @Operation(summary = "手动记录操作动态") @PostMapping() public ApiResult save(@RequestBody AppEvent appEvent) { - // 记录当前登录用户id User loginUser = getLoginUser(); if (loginUser != null) { - appEvent.setUserId(loginUser.getUserId()); + appEvent.setUserId(loginUser.getUserId()); + appEvent.setTenantId(loginUser.getTenantId()); + if (appEvent.getOperatorId() == null) { + appEvent.setOperatorId(loginUser.getUserId().longValue()); + } + if (appEvent.getOperator() == null) { + appEvent.setOperator(loginUser.getNickname()); + } } if (appEventService.save(appEvent)) { - return success("添加成功"); + return success("记录成功"); } - return fail("添加失败"); + return fail("记录失败"); } @PreAuthorize("hasAuthority('app:appEvent:update')") @OperationLog - @Operation(summary = "修改应用操作动态") + @Operation(summary = "修改操作动态") @PutMapping() public ApiResult update(@RequestBody AppEvent appEvent) { if (appEventService.updateById(appEvent)) { @@ -84,7 +91,7 @@ public class AppEventController extends BaseController { @PreAuthorize("hasAuthority('app:appEvent:remove')") @OperationLog - @Operation(summary = "删除应用操作动态") + @Operation(summary = "删除操作动态") @DeleteMapping("/{id}") public ApiResult remove(@PathVariable("id") Integer id) { if (appEventService.removeById(id)) { @@ -93,31 +100,28 @@ public class AppEventController extends BaseController { return fail("删除失败"); } - @PreAuthorize("hasAuthority('app:appEvent:save')") + @PreAuthorize("hasAuthority('app:appEvent:remove')") @OperationLog - @Operation(summary = "批量添加应用操作动态") - @PostMapping("/batch") - public ApiResult saveBatch(@RequestBody List list) { - if (appEventService.saveBatch(list)) { - return success("添加成功"); + @Operation(summary = "清空应用所有动态记录") + @DeleteMapping("/clear/{websiteId}") + public ApiResult clearByWebsiteId(@PathVariable("websiteId") Long websiteId) { + // 只清空当前租户下的数据 + AppEventParam param = new AppEventParam(); + param.setWebsiteId(websiteId); + List list = appEventService.listRel(param); + if (list.isEmpty()) { + return success("暂无动态记录"); } - return fail("添加失败"); - } - - @PreAuthorize("hasAuthority('app:appEvent:update')") - @OperationLog - @Operation(summary = "批量修改应用操作动态") - @PutMapping("/batch") - public ApiResult removeBatch(@RequestBody BatchParam batchParam) { - if (batchParam.update(appEventService, "id")) { - return success("修改成功"); + List ids = list.stream().map(AppEvent::getId).collect(java.util.stream.Collectors.toList()); + if (appEventService.removeByIds(ids)) { + return success("已清空 " + ids.size() + " 条动态记录"); } - return fail("修改失败"); + return fail("清空失败"); } @PreAuthorize("hasAuthority('app:appEvent:remove')") @OperationLog - @Operation(summary = "批量删除应用操作动态") + @Operation(summary = "批量删除操作动态") @DeleteMapping("/batch") public ApiResult removeBatch(@RequestBody List ids) { if (appEventService.removeByIds(ids)) { diff --git a/src/main/java/com/gxwebsoft/app/controller/AppUserController.java b/src/main/java/com/gxwebsoft/app/controller/AppUserController.java index 3a7f660..5500cbe 100644 --- a/src/main/java/com/gxwebsoft/app/controller/AppUserController.java +++ b/src/main/java/com/gxwebsoft/app/controller/AppUserController.java @@ -6,12 +6,12 @@ import com.gxwebsoft.app.entity.AppUser; import com.gxwebsoft.app.param.AppUserParam; import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.PageResult; -import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.system.entity.User; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -24,46 +24,42 @@ import java.util.List; * @author 科技小王子 * @since 2026-03-28 21:29:44 */ +@Slf4j @Tag(name = "应用成员管理") @RestController @RequestMapping("/api/app/app-user") public class AppUserController extends BaseController { + @Resource private AppUserService appUserService; - @PreAuthorize("hasAuthority('app:appUser:list')") @Operation(summary = "分页查询应用成员") @GetMapping("/page") public ApiResult> page(AppUserParam param) { - // 使用关联查询 return success(appUserService.pageRel(param)); } - @PreAuthorize("hasAuthority('app:appUser:list')") @Operation(summary = "查询全部应用成员") @GetMapping() public ApiResult> list(AppUserParam param) { - // 使用关联查询 return success(appUserService.listRel(param)); } - @PreAuthorize("hasAuthority('app:appUser:list')") @Operation(summary = "根据id查询应用成员") @GetMapping("/{id}") public ApiResult get(@PathVariable("id") Integer id) { - // 使用关联查询 return success(appUserService.getByIdRel(id)); } @PreAuthorize("hasAuthority('app:appUser:save')") @OperationLog - @Operation(summary = "添加应用成员") + @Operation(summary = "添加应用成员(手动添加)") @PostMapping() public ApiResult save(@RequestBody AppUser appUser) { - // 记录当前登录用户id User loginUser = getLoginUser(); if (loginUser != null) { - appUser.setUserId(loginUser.getUserId()); + appUser.setUserId(loginUser.getUserId()); + appUser.setTenantId(loginUser.getTenantId()); } if (appUserService.save(appUser)) { return success("添加成功"); @@ -71,9 +67,31 @@ public class AppUserController extends BaseController { return fail("添加失败"); } + @PreAuthorize("hasAuthority('app:appUser:save')") + @OperationLog + @Operation(summary = "邀请用户成为应用成员") + @PostMapping("/invite") + public ApiResult invite(@RequestBody AppUser appUser) { + User loginUser = getLoginUser(); + if (loginUser == null) { + return fail("请先登录"); + } + try { + AppUser result = appUserService.inviteUser( + appUser.getWebsiteId(), + appUser.getUserId(), + appUser.getRole(), + loginUser.getUserId() + ); + return success("邀请成功", result); + } catch (RuntimeException e) { + return fail(e.getMessage()); + } + } + @PreAuthorize("hasAuthority('app:appUser:update')") @OperationLog - @Operation(summary = "修改应用成员") + @Operation(summary = "修改应用成员信息") @PutMapping() public ApiResult update(@RequestBody AppUser appUser) { if (appUserService.updateById(appUser)) { @@ -82,15 +100,26 @@ public class AppUserController extends BaseController { return fail("修改失败"); } + @PreAuthorize("hasAuthority('app:appUser:update')") + @OperationLog + @Operation(summary = "修改成员角色") + @PutMapping("/role/{id}/{role}") + public ApiResult updateRole(@PathVariable("id") Long id, @PathVariable("role") String role) { + if (appUserService.updateRole(id, role)) { + return success("角色修改成功"); + } + return fail("修改失败"); + } + @PreAuthorize("hasAuthority('app:appUser:remove')") @OperationLog - @Operation(summary = "删除应用成员") + @Operation(summary = "移除应用成员") @DeleteMapping("/{id}") public ApiResult remove(@PathVariable("id") Integer id) { if (appUserService.removeById(id)) { - return success("删除成功"); + return success("已移除"); } - return fail("删除失败"); + return fail("移除失败"); } @PreAuthorize("hasAuthority('app:appUser:save')") @@ -108,7 +137,7 @@ public class AppUserController extends BaseController { @OperationLog @Operation(summary = "批量修改应用成员") @PutMapping("/batch") - public ApiResult removeBatch(@RequestBody BatchParam batchParam) { + public ApiResult updateBatch(@RequestBody BatchParam batchParam) { if (batchParam.update(appUserService, "id")) { return success("修改成功"); } @@ -117,13 +146,13 @@ public class AppUserController extends BaseController { @PreAuthorize("hasAuthority('app:appUser:remove')") @OperationLog - @Operation(summary = "批量删除应用成员") + @Operation(summary = "批量移除应用成员") @DeleteMapping("/batch") public ApiResult removeBatch(@RequestBody List ids) { if (appUserService.removeByIds(ids)) { - return success("删除成功"); + return success("移除成功"); } - return fail("删除失败"); + return fail("移除失败"); } } diff --git a/src/main/java/com/gxwebsoft/app/controller/AppVersionController.java b/src/main/java/com/gxwebsoft/app/controller/AppVersionController.java index c5e8ad2..50633fa 100644 --- a/src/main/java/com/gxwebsoft/app/controller/AppVersionController.java +++ b/src/main/java/com/gxwebsoft/app/controller/AppVersionController.java @@ -6,12 +6,12 @@ import com.gxwebsoft.app.entity.AppVersion; import com.gxwebsoft.app.param.AppVersionParam; import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.PageResult; -import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.system.entity.User; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -24,56 +24,66 @@ import java.util.List; * @author 科技小王子 * @since 2026-03-28 21:29:44 */ -@Tag(name = "应用版本发布记录管理") +@Slf4j +@Tag(name = "应用版本发布管理") @RestController @RequestMapping("/api/app/app-version") public class AppVersionController extends BaseController { + @Resource private AppVersionService appVersionService; - @PreAuthorize("hasAuthority('app:appVersion:list')") - @Operation(summary = "分页查询应用版本发布记录") + @Operation(summary = "分页查询版本记录") @GetMapping("/page") public ApiResult> page(AppVersionParam param) { - // 使用关联查询 return success(appVersionService.pageRel(param)); } - @PreAuthorize("hasAuthority('app:appVersion:list')") - @Operation(summary = "查询全部应用版本发布记录") + @Operation(summary = "查询全部版本记录") @GetMapping() public ApiResult> list(AppVersionParam param) { - // 使用关联查询 return success(appVersionService.listRel(param)); } - @PreAuthorize("hasAuthority('app:appVersion:list')") - @Operation(summary = "根据id查询应用版本发布记录") + @Operation(summary = "根据id查询版本") @GetMapping("/{id}") public ApiResult get(@PathVariable("id") Integer id) { - // 使用关联查询 return success(appVersionService.getByIdRel(id)); } + @Operation(summary = "获取应用当前版本") + @GetMapping("/current/{websiteId}") + public ApiResult getCurrentVersion(@PathVariable("websiteId") Long websiteId) { + return success(appVersionService.getCurrentVersion(websiteId)); + } + @PreAuthorize("hasAuthority('app:appVersion:save')") @OperationLog - @Operation(summary = "添加应用版本发布记录") + @Operation(summary = "新增版本(构建中状态)") @PostMapping() public ApiResult save(@RequestBody AppVersion appVersion) { - // 记录当前登录用户id User loginUser = getLoginUser(); if (loginUser != null) { - appVersion.setUserId(loginUser.getUserId()); + appVersion.setUserId(loginUser.getUserId()); + appVersion.setTenantId(loginUser.getTenantId()); } + // 默认为构建中状态 + if (appVersion.getStatus() == null) { + appVersion.setStatus(0); + } + if (appVersion.getEnv() == null) { + appVersion.setEnv("production"); + } + appVersion.setIsCurrent(false); if (appVersionService.save(appVersion)) { - return success("添加成功"); + return success("创建成功"); } - return fail("添加失败"); + return fail("创建失败"); } @PreAuthorize("hasAuthority('app:appVersion:update')") @OperationLog - @Operation(summary = "修改应用版本发布记录") + @Operation(summary = "修改版本信息") @PutMapping() public ApiResult update(@RequestBody AppVersion appVersion) { if (appVersionService.updateById(appVersion)) { @@ -82,9 +92,43 @@ public class AppVersionController extends BaseController { return fail("修改失败"); } + @PreAuthorize("hasAuthority('app:appVersion:update')") + @OperationLog + @Operation(summary = "发布版本(将此版本设为当前运行版本)") + @PostMapping("/publish/{id}") + public ApiResult publish(@PathVariable("id") Long id) { + User loginUser = getLoginUser(); + if (loginUser == null) { + return fail("请先登录"); + } + try { + appVersionService.publish(id, loginUser.getUserId()); + return success("发布成功"); + } catch (RuntimeException e) { + return fail(e.getMessage()); + } + } + + @PreAuthorize("hasAuthority('app:appVersion:update')") + @OperationLog + @Operation(summary = "回滚到指定版本") + @PostMapping("/rollback/{id}") + public ApiResult rollback(@PathVariable("id") Long id) { + User loginUser = getLoginUser(); + if (loginUser == null) { + return fail("请先登录"); + } + try { + appVersionService.rollback(id, loginUser.getUserId()); + return success("回滚成功"); + } catch (RuntimeException e) { + return fail(e.getMessage()); + } + } + @PreAuthorize("hasAuthority('app:appVersion:remove')") @OperationLog - @Operation(summary = "删除应用版本发布记录") + @Operation(summary = "删除版本记录") @DeleteMapping("/{id}") public ApiResult remove(@PathVariable("id") Integer id) { if (appVersionService.removeById(id)) { @@ -95,7 +139,7 @@ public class AppVersionController extends BaseController { @PreAuthorize("hasAuthority('app:appVersion:save')") @OperationLog - @Operation(summary = "批量添加应用版本发布记录") + @Operation(summary = "批量添加版本") @PostMapping("/batch") public ApiResult saveBatch(@RequestBody List list) { if (appVersionService.saveBatch(list)) { @@ -106,9 +150,9 @@ public class AppVersionController extends BaseController { @PreAuthorize("hasAuthority('app:appVersion:update')") @OperationLog - @Operation(summary = "批量修改应用版本发布记录") + @Operation(summary = "批量修改版本") @PutMapping("/batch") - public ApiResult removeBatch(@RequestBody BatchParam batchParam) { + public ApiResult updateBatch(@RequestBody BatchParam batchParam) { if (batchParam.update(appVersionService, "id")) { return success("修改成功"); } @@ -117,7 +161,7 @@ public class AppVersionController extends BaseController { @PreAuthorize("hasAuthority('app:appVersion:remove')") @OperationLog - @Operation(summary = "批量删除应用版本发布记录") + @Operation(summary = "批量删除版本记录") @DeleteMapping("/batch") public ApiResult removeBatch(@RequestBody List ids) { if (appVersionService.removeByIds(ids)) { diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppCredentialMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppCredentialMapper.xml index 0c2c0c0..5c709ff 100644 --- a/src/main/java/com/gxwebsoft/app/mapper/xml/AppCredentialMapper.xml +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppCredentialMapper.xml @@ -4,37 +4,29 @@ - SELECT a.* + SELECT a.*, w.website_name, w.website_code, w.website_icon FROM app_credential a + LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0 AND a.id = #{param.id} - AND a.website_id LIKE CONCAT('%', #{param.websiteId}, '%') + AND a.website_id = #{param.websiteId} - + AND a.name LIKE CONCAT('%', #{param.name}, '%') - - AND a.app_id LIKE CONCAT('%', #{param.appId}, '%') + + AND a.app_id = #{param.appId} - - AND a.app_secret LIKE CONCAT('%', #{param.appSecret}, '%') + + AND a.type = #{param.type} - - AND a.type LIKE CONCAT('%', #{param.type}, '%') - - + AND a.scopes LIKE CONCAT('%', #{param.scopes}, '%') - - AND a.expire_time LIKE CONCAT('%', #{param.expireTime}, '%') - - - AND a.last_used_at LIKE CONCAT('%', #{param.lastUsedAt}, '%') - - + AND a.remark LIKE CONCAT('%', #{param.remark}, '%') @@ -58,10 +50,11 @@ AND a.create_time <= #{param.createTimeEnd} - - AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') + + AND (a.name LIKE CONCAT('%', #{param.keywords}, '%') + OR a.app_id LIKE CONCAT('%', #{param.keywords}, '%') ) - + diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppEventMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppEventMapper.xml index ef63c66..8317ac1 100644 --- a/src/main/java/com/gxwebsoft/app/mapper/xml/AppEventMapper.xml +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppEventMapper.xml @@ -4,41 +4,30 @@ - SELECT a.* + SELECT a.*, w.website_name, w.website_code, w.website_icon FROM app_event a + LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0 AND a.id = #{param.id} - AND a.website_id LIKE CONCAT('%', #{param.websiteId}, '%') + AND a.website_id = #{param.websiteId} - - AND a.event_type LIKE CONCAT('%', #{param.eventType}, '%') + + AND a.event_type = #{param.eventType} - + AND a.title LIKE CONCAT('%', #{param.title}, '%') - - AND a.content LIKE CONCAT('%', #{param.content}, '%') - - AND a.operator_id LIKE CONCAT('%', #{param.operatorId}, '%') - - - AND a.operator LIKE CONCAT('%', #{param.operator}, '%') + AND a.operator_id = #{param.operatorId} - AND a.ref_id LIKE CONCAT('%', #{param.refId}, '%') + AND a.ref_id = #{param.refId} - - AND a.ref_type LIKE CONCAT('%', #{param.refType}, '%') - - - AND a.extra LIKE CONCAT('%', #{param.extra}, '%') - - - AND a.sort_number = #{param.sortNumber} + + AND a.ref_type = #{param.refType} AND a.status = #{param.status} @@ -46,17 +35,22 @@ AND a.user_id = #{param.userId} + + AND a.tenant_id = #{param.tenantId} + AND a.create_time >= #{param.createTimeStart} AND a.create_time <= #{param.createTimeEnd} - - AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') + + AND (a.title LIKE CONCAT('%', #{param.keywords}, '%') + OR a.content LIKE CONCAT('%', #{param.keywords}, '%') ) - + + ORDER BY a.create_time DESC diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml index e52506a..40ac1e8 100644 --- a/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml @@ -4,32 +4,30 @@ - SELECT a.* + SELECT a.*, w.website_name, w.website_code, w.website_icon, + u.nickname, u.avatar AS user_avatar, u.phone FROM app_user a + LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0 + LEFT JOIN sys_user u ON a.user_id = u.user_id AND a.id = #{param.id} - AND a.website_id LIKE CONCAT('%', #{param.websiteId}, '%') + AND a.website_id = #{param.websiteId} - AND a.user_id LIKE CONCAT('%', #{param.userId}, '%') + AND a.user_id = #{param.userId} - - AND a.username LIKE CONCAT('%', #{param.username}, '%') + + AND (a.username LIKE CONCAT('%', #{param.username}, '%') + OR u.nickname LIKE CONCAT('%', #{param.username}, '%')) - - AND a.avatar LIKE CONCAT('%', #{param.avatar}, '%') - - - AND a.role LIKE CONCAT('%', #{param.role}, '%') + + AND a.role = #{param.role} - AND a.invite_by LIKE CONCAT('%', #{param.inviteBy}, '%') - - - AND a.invite_time LIKE CONCAT('%', #{param.inviteTime}, '%') + AND a.invite_by = #{param.inviteBy} AND a.sort_number = #{param.sortNumber} @@ -37,16 +35,20 @@ AND a.status = #{param.status} + + AND a.tenant_id = #{param.tenantId} + AND a.create_time >= #{param.createTimeStart} AND a.create_time <= #{param.createTimeEnd} - - AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') + + AND (a.username LIKE CONCAT('%', #{param.keywords}, '%') + OR u.nickname LIKE CONCAT('%', #{param.keywords}, '%') ) - + diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppVersionMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppVersionMapper.xml index 23dc680..daf1699 100644 --- a/src/main/java/com/gxwebsoft/app/mapper/xml/AppVersionMapper.xml +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppVersionMapper.xml @@ -4,35 +4,24 @@ - SELECT a.* + SELECT a.*, w.website_name, w.website_code, w.website_icon FROM app_version a + LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0 AND a.id = #{param.id} - AND a.website_id LIKE CONCAT('%', #{param.websiteId}, '%') + AND a.website_id = #{param.websiteId} - - AND a.version_no LIKE CONCAT('%', #{param.versionNo}, '%') + + AND a.version_no = #{param.versionNo} - + AND a.version_name LIKE CONCAT('%', #{param.versionName}, '%') - - AND a.changelog LIKE CONCAT('%', #{param.changelog}, '%') - - - AND a.package_url LIKE CONCAT('%', #{param.packageUrl}, '%') - - - AND a.package_size LIKE CONCAT('%', #{param.packageSize}, '%') - - - AND a.package_hash LIKE CONCAT('%', #{param.packageHash}, '%') - - - AND a.env LIKE CONCAT('%', #{param.env}, '%') + + AND a.env = #{param.env} AND a.status = #{param.status} @@ -41,30 +30,26 @@ AND a.is_current = #{param.isCurrent} - AND a.publish_by LIKE CONCAT('%', #{param.publishBy}, '%') - - - AND a.publish_time LIKE CONCAT('%', #{param.publishTime}, '%') - - - AND a.remark LIKE CONCAT('%', #{param.remark}, '%') - - - AND a.sort_number = #{param.sortNumber} + AND a.publish_by = #{param.publishBy} AND a.user_id = #{param.userId} + + AND a.tenant_id = #{param.tenantId} + AND a.create_time >= #{param.createTimeStart} AND a.create_time <= #{param.createTimeEnd} - - AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') + + AND (a.version_no LIKE CONCAT('%', #{param.keywords}, '%') + OR a.version_name LIKE CONCAT('%', #{param.keywords}, '%') + OR a.changelog LIKE CONCAT('%', #{param.keywords}, '%') ) - + diff --git a/src/main/java/com/gxwebsoft/app/param/AppCredentialParam.java b/src/main/java/com/gxwebsoft/app/param/AppCredentialParam.java index cd53d0b..f40632c 100644 --- a/src/main/java/com/gxwebsoft/app/param/AppCredentialParam.java +++ b/src/main/java/com/gxwebsoft/app/param/AppCredentialParam.java @@ -1,6 +1,5 @@ package com.gxwebsoft.app.param; -import java.math.BigDecimal; import com.gxwebsoft.common.core.annotation.QueryField; import com.gxwebsoft.common.core.annotation.QueryType; import com.gxwebsoft.common.core.web.BaseParam; @@ -27,29 +26,24 @@ public class AppCredentialParam extends BaseParam { private Long id; @Schema(description = "关联应用ID") + @QueryField(type = QueryType.EQ) private Long websiteId; @Schema(description = "凭证名称,如生产环境密钥") private String name; @Schema(description = "App ID(公开)") + @QueryField(type = QueryType.EQ) private String appId; - @Schema(description = "App Secret(加密存储)") - private String appSecret; - @Schema(description = "凭证类型: server/client/webhook") + @QueryField(type = QueryType.EQ) private String type; @Schema(description = "权限范围,空格分隔") private String scopes; - @Schema(description = "到期时间,NULL=永不过期") - private String expireTime; - - @Schema(description = "最后使用时间") - private String lastUsedAt; - + @Schema(description = "备注") private String remark; @Schema(description = "排序(数字越小越靠前)") diff --git a/src/main/java/com/gxwebsoft/app/param/AppEventParam.java b/src/main/java/com/gxwebsoft/app/param/AppEventParam.java index 64b2e2a..685d390 100644 --- a/src/main/java/com/gxwebsoft/app/param/AppEventParam.java +++ b/src/main/java/com/gxwebsoft/app/param/AppEventParam.java @@ -1,6 +1,5 @@ package com.gxwebsoft.app.param; -import java.math.BigDecimal; import com.gxwebsoft.common.core.annotation.QueryField; import com.gxwebsoft.common.core.annotation.QueryType; import com.gxwebsoft.common.core.web.BaseParam; @@ -27,35 +26,27 @@ public class AppEventParam extends BaseParam { private Long id; @Schema(description = "关联应用ID") + @QueryField(type = QueryType.EQ) private Long websiteId; @Schema(description = "事件类型: created/published/updated/domain_bound/member_added/status_changed") + @QueryField(type = QueryType.EQ) private String eventType; - @Schema(description = "事件标题,如已发布") + @Schema(description = "事件标题(模糊)") private String title; - @Schema(description = "详细描述") - private String content; - @Schema(description = "操作人用户ID") + @QueryField(type = QueryType.EQ) private Long operatorId; - @Schema(description = "操作人名称(冗余)") - private String operator; - @Schema(description = "关联ID,如版本ID") + @QueryField(type = QueryType.EQ) private Long refId; @Schema(description = "关联类型") - private String refType; - - @Schema(description = "扩展数据") - private String extra; - - @Schema(description = "排序(数字越小越靠前)") @QueryField(type = QueryType.EQ) - private Integer sortNumber; + private String refType; @Schema(description = "状态, 0正常, 1冻结") @QueryField(type = QueryType.EQ) @@ -65,4 +56,8 @@ public class AppEventParam extends BaseParam { @QueryField(type = QueryType.EQ) private Integer userId; + @Schema(description = "租户ID") + @QueryField(type = QueryType.EQ) + private Integer tenantId; + } diff --git a/src/main/java/com/gxwebsoft/app/param/AppUserParam.java b/src/main/java/com/gxwebsoft/app/param/AppUserParam.java index d9578f0..cc6adea 100644 --- a/src/main/java/com/gxwebsoft/app/param/AppUserParam.java +++ b/src/main/java/com/gxwebsoft/app/param/AppUserParam.java @@ -1,6 +1,5 @@ package com.gxwebsoft.app.param; -import java.math.BigDecimal; import com.gxwebsoft.common.core.annotation.QueryField; import com.gxwebsoft.common.core.annotation.QueryType; import com.gxwebsoft.common.core.web.BaseParam; @@ -27,26 +26,24 @@ public class AppUserParam extends BaseParam { private Long id; @Schema(description = "关联应用ID") + @QueryField(type = QueryType.EQ) private Long websiteId; @Schema(description = "用户ID") - private Long userId; + @QueryField(type = QueryType.EQ) + private Integer userId; - @Schema(description = "用户名(冗余)") + @Schema(description = "用户名(模糊搜索)") private String username; - @Schema(description = "头像(冗余)") - private String avatar; - @Schema(description = "角色: owner/admin/developer/viewer") + @QueryField(type = QueryType.EQ) private String role; @Schema(description = "邀请人用户ID") + @QueryField(type = QueryType.EQ) private Long inviteBy; - @Schema(description = "加入时间") - private String inviteTime; - @Schema(description = "排序(数字越小越靠前)") @QueryField(type = QueryType.EQ) private Integer sortNumber; @@ -55,4 +52,8 @@ public class AppUserParam extends BaseParam { @QueryField(type = QueryType.EQ) private Integer status; + @Schema(description = "租户ID") + @QueryField(type = QueryType.EQ) + private Integer tenantId; + } diff --git a/src/main/java/com/gxwebsoft/app/param/AppVersionParam.java b/src/main/java/com/gxwebsoft/app/param/AppVersionParam.java index a986bc0..bf70f1a 100644 --- a/src/main/java/com/gxwebsoft/app/param/AppVersionParam.java +++ b/src/main/java/com/gxwebsoft/app/param/AppVersionParam.java @@ -1,6 +1,5 @@ package com.gxwebsoft.app.param; -import java.math.BigDecimal; import com.gxwebsoft.common.core.annotation.QueryField; import com.gxwebsoft.common.core.annotation.QueryType; import com.gxwebsoft.common.core.web.BaseParam; @@ -27,27 +26,18 @@ public class AppVersionParam extends BaseParam { private Long id; @Schema(description = "关联应用ID") + @QueryField(type = QueryType.EQ) private Long websiteId; @Schema(description = "版本号,如 1.0.0") + @QueryField(type = QueryType.EQ) private String versionNo; - @Schema(description = "版本名称") + @Schema(description = "版本名称(模糊)") private String versionName; - @Schema(description = "版本更新说明") - private String changelog; - - @Schema(description = "安装包地址") - private String packageUrl; - - @Schema(description = "包大小(字节)") - private Long packageSize; - - @Schema(description = "包MD5/SHA256") - private String packageHash; - @Schema(description = "环境: development/staging/production") + @QueryField(type = QueryType.EQ) private String env; @Schema(description = "状态 0=构建中 1=已发布 2=已回滚 3=构建失败") @@ -59,20 +49,15 @@ public class AppVersionParam extends BaseParam { private Boolean isCurrent; @Schema(description = "发布人用户ID") - private Long publishBy; - - @Schema(description = "发布时间") - private String publishTime; - - @Schema(description = "备注") - private String remark; - - @Schema(description = "排序(数字越小越靠前)") @QueryField(type = QueryType.EQ) - private Integer sortNumber; + private Long publishBy; @Schema(description = "用户ID") @QueryField(type = QueryType.EQ) private Integer userId; + @Schema(description = "租户ID") + @QueryField(type = QueryType.EQ) + private Integer tenantId; + } diff --git a/src/main/java/com/gxwebsoft/app/service/AppCredentialService.java b/src/main/java/com/gxwebsoft/app/service/AppCredentialService.java index 6a426ce..ddd5d66 100644 --- a/src/main/java/com/gxwebsoft/app/service/AppCredentialService.java +++ b/src/main/java/com/gxwebsoft/app/service/AppCredentialService.java @@ -17,26 +17,41 @@ public interface AppCredentialService extends IService { /** * 分页关联查询 - * - * @param param 查询参数 - * @return PageResult */ PageResult pageRel(AppCredentialParam param); /** * 关联查询全部 - * - * @param param 查询参数 - * @return List */ List listRel(AppCredentialParam param); /** * 根据id查询 - * - * @param id 自增ID - * @return AppCredential */ AppCredential getByIdRel(Integer id); + /** + * 创建凭证(自动生成 appId 和 appSecret) + * + * @param credential 凭证基础信息 + * @return 包含明文 appSecret 的凭证对象(仅此次返回,之后不再展示) + */ + AppCredential createCredential(AppCredential credential); + + /** + * 重置密钥(重新生成 appSecret) + * + * @param id 凭证ID + * @return 包含新明文 appSecret 的凭证对象 + */ + AppCredential resetSecret(Long id); + + /** + * 禁用/启用凭证 + * + * @param id 凭证ID + * @param status 0=正常 1=冻结 + */ + boolean updateStatus(Long id, Integer status); + } diff --git a/src/main/java/com/gxwebsoft/app/service/AppEventService.java b/src/main/java/com/gxwebsoft/app/service/AppEventService.java index 34d8d89..d0a8b0f 100644 --- a/src/main/java/com/gxwebsoft/app/service/AppEventService.java +++ b/src/main/java/com/gxwebsoft/app/service/AppEventService.java @@ -17,26 +17,44 @@ public interface AppEventService extends IService { /** * 分页关联查询 - * - * @param param 查询参数 - * @return PageResult */ PageResult pageRel(AppEventParam param); /** * 关联查询全部 - * - * @param param 查询参数 - * @return List */ List listRel(AppEventParam param); /** * 根据id查询 - * - * @param id 自增ID - * @return AppEvent */ AppEvent getByIdRel(Integer id); + /** + * 记录应用事件(便捷方法,供其他模块调用) + * + * @param websiteId 应用ID + * @param eventType 事件类型(created/published/updated/domain_bound/member_added/status_changed 等) + * @param title 事件标题 + * @param content 详细描述 + * @param operatorId 操作人用户ID + * @param operatorName 操作人名称 + * @param refId 关联记录ID(如版本ID) + * @param refType 关联记录类型 + */ + void logEvent(Long websiteId, String eventType, String title, String content, + Long operatorId, String operatorName, Long refId, String refType); + + /** + * 快捷记录事件(无关联记录) + */ + void logEvent(Long websiteId, String eventType, String title, Long operatorId, String operatorName); + + /** + * 获取指定应用的最新一条事件(用于卡片"最新动态"展示) + * + * @param websiteId 应用ID + */ + AppEvent getLatestEvent(Long websiteId); + } diff --git a/src/main/java/com/gxwebsoft/app/service/AppUserService.java b/src/main/java/com/gxwebsoft/app/service/AppUserService.java index 82706e9..6888fc2 100644 --- a/src/main/java/com/gxwebsoft/app/service/AppUserService.java +++ b/src/main/java/com/gxwebsoft/app/service/AppUserService.java @@ -17,26 +17,44 @@ public interface AppUserService extends IService { /** * 分页关联查询 - * - * @param param 查询参数 - * @return PageResult */ PageResult pageRel(AppUserParam param); /** * 关联查询全部 - * - * @param param 查询参数 - * @return List */ List listRel(AppUserParam param); /** * 根据id查询 - * - * @param id 自增ID - * @return AppUser */ AppUser getByIdRel(Integer id); + /** + * 邀请成员加入应用 + * + * @param websiteId 应用ID + * @param userId 被邀请的用户ID + * @param role 分配的角色 + * @param inviteBy 邀请人用户ID + * @return 成员记录 + */ + AppUser inviteUser(Long websiteId, Integer userId, String role, Integer inviteBy); + + /** + * 修改成员角色 + * + * @param id 成员记录ID + * @param role 新角色 + */ + boolean updateRole(Long id, String role); + + /** + * 检查用户是否已是应用成员 + * + * @param websiteId 应用ID + * @param userId 用户ID + */ + boolean isMember(Long websiteId, Integer userId); + } diff --git a/src/main/java/com/gxwebsoft/app/service/AppVersionService.java b/src/main/java/com/gxwebsoft/app/service/AppVersionService.java index 8a56e0e..bbfa879 100644 --- a/src/main/java/com/gxwebsoft/app/service/AppVersionService.java +++ b/src/main/java/com/gxwebsoft/app/service/AppVersionService.java @@ -17,26 +17,40 @@ public interface AppVersionService extends IService { /** * 分页关联查询 - * - * @param param 查询参数 - * @return PageResult */ PageResult pageRel(AppVersionParam param); /** * 关联查询全部 - * - * @param param 查询参数 - * @return List */ List listRel(AppVersionParam param); /** * 根据id查询 - * - * @param id 自增ID - * @return AppVersion */ AppVersion getByIdRel(Integer id); + /** + * 发布版本(将此版本设为当前版本,其他版本 isCurrent=false) + * + * @param id 版本ID + * @param publishBy 发布人用户ID + */ + boolean publish(Long id, Integer publishBy); + + /** + * 回滚到指定版本(将此版本设为当前版本,当前版本标为已回滚) + * + * @param id 要回滚到的版本ID + * @param publishBy 操作人用户ID + */ + boolean rollback(Long id, Integer publishBy); + + /** + * 获取指定应用的当前版本 + * + * @param websiteId 应用ID + */ + AppVersion getCurrentVersion(Long websiteId); + } diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppCredentialServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppCredentialServiceImpl.java index 3028fde..cf2de6b 100644 --- a/src/main/java/com/gxwebsoft/app/service/impl/AppCredentialServiceImpl.java +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppCredentialServiceImpl.java @@ -1,12 +1,17 @@ package com.gxwebsoft.app.service.impl; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.gxwebsoft.app.mapper.AppCredentialMapper; import com.gxwebsoft.app.service.AppCredentialService; import com.gxwebsoft.app.entity.AppCredential; import com.gxwebsoft.app.param.AppCredentialParam; +import com.gxwebsoft.common.core.utils.CommonUtil; import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageResult; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; @@ -17,6 +22,7 @@ import java.util.List; * @author 科技小王子 * @since 2026-03-28 21:29:43 */ +@Slf4j @Service public class AppCredentialServiceImpl extends ServiceImpl implements AppCredentialService { @@ -25,23 +31,95 @@ public class AppCredentialServiceImpl extends ServiceImpl page = new PageParam<>(param); page.setDefaultOrder("sort_number asc, create_time desc"); List list = baseMapper.selectPageRel(page, param); + // 脱敏处理:appSecret 不对外展示(仅创建/重置时返回明文) + list.forEach(this::maskSecret); return new PageResult<>(list, page.getTotal()); } @Override public List listRel(AppCredentialParam param) { List list = baseMapper.selectListRel(param); - // 排序 PageParam page = new PageParam<>(); page.setDefaultOrder("sort_number asc, create_time desc"); - return page.sortRecords(list); + list = page.sortRecords(list); + list.forEach(this::maskSecret); + return list; } @Override public AppCredential getByIdRel(Integer id) { AppCredentialParam param = new AppCredentialParam(); - param.setId(id); - return param.getOne(baseMapper.selectListRel(param)); + param.setId(id.longValue()); + AppCredential credential = param.getOne(baseMapper.selectListRel(param)); + if (credential != null) { + maskSecret(credential); + } + return credential; + } + + @Override + public AppCredential createCredential(AppCredential credential) { + // 生成 AppID:app_ + 16位随机字符串 + String appId = "app_" + CommonUtil.randomUUID16(); + // 生成 AppSecret:32位随机字符串(大小写字母+数字) + String appSecret = generateSecretValue(); + + credential.setAppId(appId); + credential.setAppSecret(appSecret); + // 默认状态正常 + if (credential.getStatus() == null) { + credential.setStatus(0); + } + if (StrUtil.isBlank(credential.getType())) { + credential.setType("server"); + } + + save(credential); + log.info("创建凭证成功,websiteId={}, appId={}", credential.getWebsiteId(), appId); + // 返回含明文 secret 的对象(仅此一次) + return credential; + } + + @Override + public AppCredential resetSecret(Long id) { + AppCredential credential = getById(id); + if (credential == null) { + throw new RuntimeException("凭证不存在"); + } + String newSecret = generateSecretValue(); + credential.setAppSecret(newSecret); + updateById(credential); + log.info("重置凭证密钥成功,id={}", id); + return credential; + } + + @Override + public boolean updateStatus(Long id, Integer status) { + return update(new LambdaUpdateWrapper() + .eq(AppCredential::getId, id) + .set(AppCredential::getStatus, status)); + } + + /** + * 生成32位随机 AppSecret + */ + private String generateSecretValue() { + // 格式:8位-8位-8位-8位(类似 UUID 格式,便于复制) + return RandomUtil.randomString(8) + "-" + + RandomUtil.randomString(8) + "-" + + RandomUtil.randomString(8) + "-" + + RandomUtil.randomString(8); + } + + /** + * 脱敏处理:将 appSecret 替换为掩码 + */ + private void maskSecret(AppCredential credential) { + if (StrUtil.isNotBlank(credential.getAppSecret())) { + // 只保留前4位,其余用 * 替代 + String secret = credential.getAppSecret(); + credential.setAppSecret(secret.substring(0, Math.min(4, secret.length())) + "**********************"); + } } } diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppEventServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppEventServiceImpl.java index 91c147a..bcdb7c8 100644 --- a/src/main/java/com/gxwebsoft/app/service/impl/AppEventServiceImpl.java +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppEventServiceImpl.java @@ -1,5 +1,6 @@ package com.gxwebsoft.app.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.gxwebsoft.app.mapper.AppEventMapper; import com.gxwebsoft.app.service.AppEventService; @@ -7,6 +8,8 @@ import com.gxwebsoft.app.entity.AppEvent; import com.gxwebsoft.app.param.AppEventParam; import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.List; @@ -17,13 +20,14 @@ import java.util.List; * @author 科技小王子 * @since 2026-03-28 21:29:44 */ +@Slf4j @Service public class AppEventServiceImpl extends ServiceImpl implements AppEventService { @Override public PageResult pageRel(AppEventParam param) { PageParam page = new PageParam<>(param); - page.setDefaultOrder("sort_number asc, create_time desc"); + page.setDefaultOrder("create_time desc"); List list = baseMapper.selectPageRel(page, param); return new PageResult<>(list, page.getTotal()); } @@ -31,17 +35,52 @@ public class AppEventServiceImpl extends ServiceImpl i @Override public List listRel(AppEventParam param) { List list = baseMapper.selectListRel(param); - // 排序 PageParam page = new PageParam<>(); - page.setDefaultOrder("sort_number asc, create_time desc"); + page.setDefaultOrder("create_time desc"); return page.sortRecords(list); } @Override public AppEvent getByIdRel(Integer id) { AppEventParam param = new AppEventParam(); - param.setId(id); + param.setId(id.longValue()); return param.getOne(baseMapper.selectListRel(param)); } + @Override + @Async + public void logEvent(Long websiteId, String eventType, String title, String content, + Long operatorId, String operatorName, Long refId, String refType) { + try { + AppEvent event = new AppEvent(); + event.setWebsiteId(websiteId); + event.setEventType(eventType); + event.setTitle(title); + event.setContent(content); + event.setOperatorId(operatorId); + event.setOperator(operatorName); + event.setRefId(refId); + event.setRefType(refType); + event.setStatus(0); + save(event); + log.debug("记录事件成功,websiteId={}, eventType={}, title={}", websiteId, eventType, title); + } catch (Exception e) { + log.warn("记录事件失败,websiteId={}, eventType={}: {}", websiteId, eventType, e.getMessage()); + } + } + + @Override + @Async + public void logEvent(Long websiteId, String eventType, String title, Long operatorId, String operatorName) { + logEvent(websiteId, eventType, title, null, operatorId, operatorName, null, null); + } + + @Override + public AppEvent getLatestEvent(Long websiteId) { + return getOne(new LambdaQueryWrapper() + .eq(AppEvent::getWebsiteId, websiteId) + .orderByDesc(AppEvent::getCreateTime) + .last("LIMIT 1")); + } + } diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java index 2f2fb01..47ff990 100644 --- a/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java @@ -1,5 +1,8 @@ package com.gxwebsoft.app.service.impl; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.gxwebsoft.app.mapper.AppUserMapper; import com.gxwebsoft.app.service.AppUserService; @@ -7,8 +10,10 @@ import com.gxwebsoft.app.entity.AppUser; import com.gxwebsoft.app.param.AppUserParam; import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageResult; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.List; /** @@ -17,13 +22,14 @@ import java.util.List; * @author 科技小王子 * @since 2026-03-28 21:29:44 */ +@Slf4j @Service public class AppUserServiceImpl extends ServiceImpl implements AppUserService { @Override public PageResult pageRel(AppUserParam param) { PageParam page = new PageParam<>(param); - page.setDefaultOrder("sort_number asc, create_time desc"); + page.setDefaultOrder("sort_number asc, create_time asc"); List list = baseMapper.selectPageRel(page, param); return new PageResult<>(list, page.getTotal()); } @@ -31,17 +37,53 @@ public class AppUserServiceImpl extends ServiceImpl impl @Override public List listRel(AppUserParam param) { List list = baseMapper.selectListRel(param); - // 排序 PageParam page = new PageParam<>(); - page.setDefaultOrder("sort_number asc, create_time desc"); + page.setDefaultOrder("sort_number asc, create_time asc"); return page.sortRecords(list); } @Override public AppUser getByIdRel(Integer id) { AppUserParam param = new AppUserParam(); - param.setId(id); + param.setId(id.longValue()); return param.getOne(baseMapper.selectListRel(param)); } + @Override + public AppUser inviteUser(Long websiteId, Integer userId, String role, Integer inviteBy) { + // 检查是否已经是成员 + if (isMember(websiteId, userId)) { + throw new RuntimeException("该用户已经是应用成员"); + } + + AppUser appUser = new AppUser(); + appUser.setWebsiteId(websiteId); + appUser.setUserId(userId); + appUser.setRole(role != null ? role : "developer"); + appUser.setInviteBy(inviteBy.longValue()); + appUser.setInviteTime(LocalDateTime.now()); + appUser.setStatus(0); + appUser.setSortNumber(0); + + save(appUser); + log.info("邀请成员成功,websiteId={}, userId={}, role={}", websiteId, userId, role); + return appUser; + } + + @Override + public boolean updateRole(Long id, String role) { + return update(new LambdaUpdateWrapper() + .eq(AppUser::getId, id) + .set(AppUser::getRole, role)); + } + + @Override + public boolean isMember(Long websiteId, Integer userId) { + long count = count(new LambdaQueryWrapper() + .eq(AppUser::getWebsiteId, websiteId) + .eq(AppUser::getUserId, userId) + .eq(AppUser::getStatus, 0)); + return count > 0; + } + } diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppVersionServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppVersionServiceImpl.java index 3a5e90b..b45446f 100644 --- a/src/main/java/com/gxwebsoft/app/service/impl/AppVersionServiceImpl.java +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppVersionServiceImpl.java @@ -1,5 +1,7 @@ package com.gxwebsoft.app.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.gxwebsoft.app.mapper.AppVersionMapper; import com.gxwebsoft.app.service.AppVersionService; @@ -7,8 +9,11 @@ import com.gxwebsoft.app.entity.AppVersion; import com.gxwebsoft.app.param.AppVersionParam; import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageResult; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; /** @@ -17,13 +22,14 @@ import java.util.List; * @author 科技小王子 * @since 2026-03-28 21:29:44 */ +@Slf4j @Service public class AppVersionServiceImpl extends ServiceImpl implements AppVersionService { @Override public PageResult pageRel(AppVersionParam param) { PageParam page = new PageParam<>(param); - page.setDefaultOrder("sort_number asc, create_time desc"); + page.setDefaultOrder("create_time desc"); List list = baseMapper.selectPageRel(page, param); return new PageResult<>(list, page.getTotal()); } @@ -31,17 +37,76 @@ public class AppVersionServiceImpl extends ServiceImpl listRel(AppVersionParam param) { List list = baseMapper.selectListRel(param); - // 排序 PageParam page = new PageParam<>(); - page.setDefaultOrder("sort_number asc, create_time desc"); + page.setDefaultOrder("create_time desc"); return page.sortRecords(list); } @Override public AppVersion getByIdRel(Integer id) { AppVersionParam param = new AppVersionParam(); - param.setId(id); + param.setId(id.longValue()); return param.getOne(baseMapper.selectListRel(param)); } + @Override + @Transactional(rollbackFor = Exception.class) + public boolean publish(Long id, Integer publishBy) { + AppVersion version = getById(id); + if (version == null) { + throw new RuntimeException("版本不存在"); + } + Long websiteId = version.getWebsiteId(); + + // 1. 将该应用下所有版本的 isCurrent 设为 false + update(new LambdaUpdateWrapper() + .eq(AppVersion::getWebsiteId, websiteId) + .set(AppVersion::getIsCurrent, false)); + + // 2. 将当前版本设为已发布+当前版本 + version.setStatus(1); + version.setIsCurrent(true); + version.setPublishBy(publishBy.longValue()); + version.setPublishTime(LocalDateTime.now()); + + boolean result = updateById(version); + log.info("发布版本成功,id={}, versionNo={}", id, version.getVersionNo()); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean rollback(Long id, Integer publishBy) { + AppVersion targetVersion = getById(id); + if (targetVersion == null) { + throw new RuntimeException("目标版本不存在"); + } + Long websiteId = targetVersion.getWebsiteId(); + + // 1. 将当前版本标记为已回滚 + update(new LambdaUpdateWrapper() + .eq(AppVersion::getWebsiteId, websiteId) + .eq(AppVersion::getIsCurrent, true) + .set(AppVersion::getStatus, 2) // 2=已回滚 + .set(AppVersion::getIsCurrent, false)); + + // 2. 将目标版本设为当前版本 + targetVersion.setStatus(1); + targetVersion.setIsCurrent(true); + targetVersion.setPublishBy(publishBy.longValue()); + targetVersion.setPublishTime(LocalDateTime.now()); + + boolean result = updateById(targetVersion); + log.info("回滚版本成功,id={}, versionNo={}", id, targetVersion.getVersionNo()); + return result; + } + + @Override + public AppVersion getCurrentVersion(Long websiteId) { + return getOne(new LambdaQueryWrapper() + .eq(AppVersion::getWebsiteId, websiteId) + .eq(AppVersion::getIsCurrent, true) + .last("LIMIT 1")); + } + } From 17352718c5bca3b8ed2fabeb12ccdf53c6ea254d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sun, 29 Mar 2026 00:31:00 +0800 Subject: [PATCH 03/21] =?UTF-8?q?chore(config):=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=BF=80=E6=B4=BB=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=BA=E5=BC=80=E5=8F=91=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 spring.profiles.active 从 ysb2 更改为 dev - 更新默认运行环境配置以适应开发需求 --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dcba0f0..ada4ffe 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,7 +4,7 @@ server: # 多环境配置 spring: profiles: - active: ysb2 + active: dev application: name: server From 6aeface82da5197b19eb20caea5c721da2d5fdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sun, 29 Mar 2026 00:52:46 +0800 Subject: [PATCH 04/21] =?UTF-8?q?fix(database):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=A1=A8=E5=85=B3=E8=81=94=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E7=9A=84=E6=95=B0=E6=8D=AE=E5=BA=93=E6=A8=A1=E5=BC=8F=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 AppUserMapper.xml 中的 LEFT JOIN 语句 - 为 sys_user 表添加 gxwebsoft_core 数据库前缀 - 确保跨数据库关联查询的正确性 - 保持与系统用户表的数据一致性 --- src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml index 40ac1e8..46d080d 100644 --- a/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml @@ -8,7 +8,7 @@ u.nickname, u.avatar AS user_avatar, u.phone FROM app_user a LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0 - LEFT JOIN sys_user u ON a.user_id = u.user_id + LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id AND a.id = #{param.id} From 75c13bada869e2fabb62125b109a1a5f86964ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sun, 29 Mar 2026 01:19:48 +0800 Subject: [PATCH 05/21] =?UTF-8?q?refactor(app-user):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E8=B7=A8=E5=BA=93JOIN=E4=BE=9D=E8=B5=96=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E5=86=97=E4=BD=99=E5=AD=97=E6=AE=B5=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在AppUser实体中新增nickname和phone冗余字段 - 移除AppUserMapper.xml中的跨库JOIN查询gxwebsoft_core.sys_user表 - 更新搜索条件使用本地冗余字段而非关联表字段 - 注入UserService用于写入冗余用户基础信息 - 邀请成员时查询系统用户信息并冗余存储到本地表 - 日志输出中增加用户名信息便于调试追踪 --- .../com/gxwebsoft/app/entity/AppUser.java | 6 ++++++ .../app/mapper/xml/AppUserMapper.xml | 11 ++++------ .../app/service/impl/AppUserServiceImpl.java | 20 ++++++++++++++++++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/gxwebsoft/app/entity/AppUser.java b/src/main/java/com/gxwebsoft/app/entity/AppUser.java index a5d3ad3..09ade12 100644 --- a/src/main/java/com/gxwebsoft/app/entity/AppUser.java +++ b/src/main/java/com/gxwebsoft/app/entity/AppUser.java @@ -35,9 +35,15 @@ public class AppUser implements Serializable { @Schema(description = "用户名(冗余)") private String username; + @Schema(description = "昵称(冗余)") + private String nickname; + @Schema(description = "头像(冗余)") private String avatar; + @Schema(description = "手机号(冗余,脱敏存储)") + private String phone; + @Schema(description = "角色: owner/admin/developer/viewer") private String role; diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml index 46d080d..eab0732 100644 --- a/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppUserMapper.xml @@ -2,13 +2,11 @@ - + - SELECT a.*, w.website_name, w.website_code, w.website_icon, - u.nickname, u.avatar AS user_avatar, u.phone + SELECT a.*, w.website_name, w.website_code, w.website_icon FROM app_user a LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0 - LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id AND a.id = #{param.id} @@ -21,7 +19,7 @@ AND (a.username LIKE CONCAT('%', #{param.username}, '%') - OR u.nickname LIKE CONCAT('%', #{param.username}, '%')) + OR a.nickname LIKE CONCAT('%', #{param.username}, '%')) AND a.role = #{param.role} @@ -46,8 +44,7 @@ AND (a.username LIKE CONCAT('%', #{param.keywords}, '%') - OR u.nickname LIKE CONCAT('%', #{param.keywords}, '%') - ) + OR a.nickname LIKE CONCAT('%', #{param.keywords}, '%')) diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java index 47ff990..0732a7d 100644 --- a/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java @@ -10,9 +10,12 @@ import com.gxwebsoft.app.entity.AppUser; import com.gxwebsoft.app.param.AppUserParam; import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.common.system.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.List; @@ -26,6 +29,10 @@ import java.util.List; @Service public class AppUserServiceImpl extends ServiceImpl implements AppUserService { + /** 注入同 jar 包内的 UserService,用于写入冗余用户信息,不做跨库 JOIN */ + @Resource + private UserService userService; + @Override public PageResult pageRel(AppUserParam param) { PageParam page = new PageParam<>(param); @@ -56,6 +63,12 @@ public class AppUserServiceImpl extends ServiceImpl impl throw new RuntimeException("该用户已经是应用成员"); } + // 查询用户基础信息并冗余写入,避免跨库 JOIN + User sysUser = userService.getByIdIgnoreTenant(userId); + if (sysUser == null) { + throw new RuntimeException("用户不存在,userId=" + userId); + } + AppUser appUser = new AppUser(); appUser.setWebsiteId(websiteId); appUser.setUserId(userId); @@ -64,9 +77,14 @@ public class AppUserServiceImpl extends ServiceImpl impl appUser.setInviteTime(LocalDateTime.now()); appUser.setStatus(0); appUser.setSortNumber(0); + // 冗余写入用户基础信息,彻底解除跨库 JOIN 依赖 + appUser.setUsername(sysUser.getUsername()); + appUser.setNickname(sysUser.getNickname()); + appUser.setAvatar(sysUser.getAvatar()); + appUser.setPhone(sysUser.getPhone()); save(appUser); - log.info("邀请成员成功,websiteId={}, userId={}, role={}", websiteId, userId, role); + log.info("邀请成员成功,websiteId={}, userId={}, username={}, role={}", websiteId, userId, sysUser.getUsername(), role); return appUser; } From bd2a92d83239bd72b0378654523a1bb55e040b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sun, 29 Mar 2026 02:36:07 +0800 Subject: [PATCH 06/21] =?UTF-8?q?feat(website):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E7=B1=BB=E5=9E=8B=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E6=A0=87=E8=AF=86=E7=AC=A6=E5=94=AF=E4=B8=80=E6=80=A7=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除邀请用户接口的权限注解和操作日志注解 - 更新CmsWebsite实体类中的类型描述,支持多种应用类型 - 在保存和修改网站信息时增加websiteCode唯一性校验逻辑 - 实现create方法中根据应用类型自动设置管理后台地址和类型名称 - 添加generateUniqueCode方法确保websiteCode全局唯一 - 优化代码结构和业务逻辑处理 --- .../app/controller/AppUserController.java | 2 - .../cms/controller/CmsWebsiteController.java | 24 +++++++ .../com/gxwebsoft/cms/entity/CmsWebsite.java | 2 +- .../service/impl/CmsWebsiteServiceImpl.java | 66 ++++++++++++++++--- 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/gxwebsoft/app/controller/AppUserController.java b/src/main/java/com/gxwebsoft/app/controller/AppUserController.java index 5500cbe..4124862 100644 --- a/src/main/java/com/gxwebsoft/app/controller/AppUserController.java +++ b/src/main/java/com/gxwebsoft/app/controller/AppUserController.java @@ -67,8 +67,6 @@ public class AppUserController extends BaseController { return fail("添加失败"); } - @PreAuthorize("hasAuthority('app:appUser:save')") - @OperationLog @Operation(summary = "邀请用户成为应用成员") @PostMapping("/invite") public ApiResult invite(@RequestBody AppUser appUser) { diff --git a/src/main/java/com/gxwebsoft/cms/controller/CmsWebsiteController.java b/src/main/java/com/gxwebsoft/cms/controller/CmsWebsiteController.java index 8c6c045..0a1ade6 100644 --- a/src/main/java/com/gxwebsoft/cms/controller/CmsWebsiteController.java +++ b/src/main/java/com/gxwebsoft/cms/controller/CmsWebsiteController.java @@ -96,6 +96,16 @@ public class CmsWebsiteController extends BaseController { @Operation(summary = "添加网站信息记录表") @PostMapping() public ApiResult save(@RequestBody CmsWebsite cmsWebsite) { + // 前端若指定了 websiteCode,先做唯一性校验 + if (StrUtil.isNotBlank(cmsWebsite.getWebsiteCode())) { + long cnt = cmsWebsiteService.count( + new LambdaQueryWrapper() + .eq(CmsWebsite::getWebsiteCode, cmsWebsite.getWebsiteCode()) + ); + if (cnt > 0) { + return fail("应用标识 [" + cmsWebsite.getWebsiteCode() + "] 已存在,请更换"); + } + } // 记录当前登录用户id User loginUser = getLoginUser(); if (loginUser != null) { @@ -109,6 +119,20 @@ public class CmsWebsiteController extends BaseController { @Operation(summary = "修改网站信息记录表") @PutMapping() public ApiResult update(@RequestBody CmsWebsite cmsWebsite) { + // websiteCode 全局唯一,有值时校验是否与其他记录冲突(排除自身) + if (StrUtil.isNotBlank(cmsWebsite.getWebsiteCode())) { + long cnt = cmsWebsiteService.count( + new LambdaQueryWrapper() + .eq(CmsWebsite::getWebsiteCode, cmsWebsite.getWebsiteCode()) + .ne(CmsWebsite::getWebsiteId, cmsWebsite.getWebsiteId()) + ); + if (cnt > 0) { + return fail("应用标识 [" + cmsWebsite.getWebsiteCode() + "] 已存在,请更换"); + } + } else { + // 不允许通过此接口清空或修改 websiteCode(设为 null 则 MP 不更新该字段) + cmsWebsite.setWebsiteCode(null); + } if (cmsWebsiteService.updateById(cmsWebsite)) { return success("修改成功"); } diff --git a/src/main/java/com/gxwebsoft/cms/entity/CmsWebsite.java b/src/main/java/com/gxwebsoft/cms/entity/CmsWebsite.java index 46bec1c..31087c4 100644 --- a/src/main/java/com/gxwebsoft/cms/entity/CmsWebsite.java +++ b/src/main/java/com/gxwebsoft/cms/entity/CmsWebsite.java @@ -66,7 +66,7 @@ public class CmsWebsite implements Serializable { @Schema(description = "网站截图") private String files; - @Schema(description = "网站类型 10企业官网 20微信小程序 30APP 40其他") + @Schema(description = "应用类型 10=web(Web应用) 20=miniprogram(小程序) 30=mobile(移动App) 40=api(API服务) 50=internal(内部工具)") private Integer type; @Schema(description = "网站关键词") diff --git a/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java b/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java index 348d3e5..8b74f83 100644 --- a/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java +++ b/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java @@ -145,15 +145,45 @@ public class CmsWebsiteServiceImpl extends ServiceImpl 0) { // TODO 国际化 @@ -432,4 +462,22 @@ public class CmsWebsiteServiceImpl extends ServiceImpl bottomNavs = cmsNavigationService.listRel(navigationParam); website.setBottomNavs(bottomNavs); } + + /** + * 生成全局唯一的 websiteCode。 + *

以 baseCode 为基础,若已存在则追加 -2、-3 … 直到找到空闲值。

+ * + * @param baseCode 期望的 code 前缀,如 "site-10398" + * @return 全局唯一的 websiteCode + */ + private String generateUniqueCode(String baseCode) { + String candidate = baseCode; + int suffix = 2; + while (count(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(CmsWebsite::getWebsiteCode, candidate)) > 0) { + candidate = baseCode + "-" + suffix; + suffix++; + } + return candidate; + } } From df7a41f3c49963db550678f074ed748027268fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 30 Mar 2026 11:08:12 +0800 Subject: [PATCH 07/21] =?UTF-8?q?feat(contact):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=81=94=E7=B3=BB=E8=A1=A8=E5=8D=95=E9=94=80=E5=94=AE=E7=BA=BF?= =?UTF-8?q?=E7=B4=A2=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增联系表单数据库表结构设计与实体类定义 - 实现联系表单提交接口,支持公开访问无需登录验证 - 添加后台管理接口,支持线索查询、状态更新和删除操作 - 集成企业微信和飞书机器人通知功能,实时推送新线索 - 在安全配置中开放联系表单提交接口访问权限 - 添加应用配置中的通知机器人Webhook配置项 --- .../controller/CmsContactLeadController.java | 142 ++++++++++++ .../gxwebsoft/cms/entity/CmsContactLead.java | 83 +++++++ .../cms/mapper/CmsContactLeadMapper.java | 36 +++ .../cms/mapper/xml/CmsContactLeadMapper.xml | 66 ++++++ .../cms/param/CmsContactLeadParam.java | 56 +++++ .../cms/service/CmsContactLeadService.java | 42 ++++ .../impl/CmsContactLeadServiceImpl.java | 209 ++++++++++++++++++ .../gxwebsoft/cms/sql/cms_contact_lead.sql | 25 +++ .../common/core/security/SecurityConfig.java | 3 +- src/main/resources/application.yml | 15 +- 10 files changed, 670 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/gxwebsoft/cms/controller/CmsContactLeadController.java create mode 100644 src/main/java/com/gxwebsoft/cms/entity/CmsContactLead.java create mode 100644 src/main/java/com/gxwebsoft/cms/mapper/CmsContactLeadMapper.java create mode 100644 src/main/java/com/gxwebsoft/cms/mapper/xml/CmsContactLeadMapper.xml create mode 100644 src/main/java/com/gxwebsoft/cms/param/CmsContactLeadParam.java create mode 100644 src/main/java/com/gxwebsoft/cms/service/CmsContactLeadService.java create mode 100644 src/main/java/com/gxwebsoft/cms/service/impl/CmsContactLeadServiceImpl.java create mode 100644 src/main/java/com/gxwebsoft/cms/sql/cms_contact_lead.sql diff --git a/src/main/java/com/gxwebsoft/cms/controller/CmsContactLeadController.java b/src/main/java/com/gxwebsoft/cms/controller/CmsContactLeadController.java new file mode 100644 index 0000000..2b7d6d9 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/controller/CmsContactLeadController.java @@ -0,0 +1,142 @@ +package com.gxwebsoft.cms.controller; + +import com.gxwebsoft.cms.entity.CmsContactLead; +import com.gxwebsoft.cms.param.CmsContactLeadParam; +import com.gxwebsoft.cms.service.CmsContactLeadService; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.common.core.web.BatchParam; +import com.gxwebsoft.common.core.web.PageResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import java.util.List; + +/** + * 联系表单/销售线索控制器 + * + * @author 科技小王子 + * @since 2026-03-30 + */ +@Slf4j +@Validated +@Tag(name = "联系表单/销售线索管理") +@RestController +@RequestMapping("/api/cms/cms-contact-lead") +public class CmsContactLeadController extends BaseController { + + @Resource + private CmsContactLeadService cmsContactLeadService; + + // ------------------------- + // 公开接口(无需登录) + // ------------------------- + + /** + * 提交联系表单(官网 /contact 页调用,不需要登录) + */ + @Operation(summary = "提交联系表单") + @PostMapping("/submit") + public ApiResult submit(@RequestBody @Valid CmsContactLead lead, HttpServletRequest request) { + // 获取客户端真实IP + String ip = getClientIp(request); + if (cmsContactLeadService.submit(lead, ip)) { + return success("提交成功,我们将尽快与您联系!"); + } + return fail("提交失败,请稍后重试"); + } + + // ------------------------- + // 后台管理接口(需要权限) + // ------------------------- + + @Operation(summary = "分页查询线索列表") + @PreAuthorize("hasAuthority('cms:contactLead:list')") + @GetMapping("/page") + public ApiResult> page(CmsContactLeadParam param) { + return success(cmsContactLeadService.pageRel(param)); + } + + @Operation(summary = "查询全部线索") + @PreAuthorize("hasAuthority('cms:contactLead:list')") + @GetMapping() + public ApiResult> list(CmsContactLeadParam param) { + return success(cmsContactLeadService.listRel(param)); + } + + @Operation(summary = "根据ID查询线索") + @PreAuthorize("hasAuthority('cms:contactLead:list')") + @GetMapping("/{id}") + public ApiResult get(@PathVariable("id") Integer id) { + return success(cmsContactLeadService.getById(id)); + } + + @Operation(summary = "更新跟进状态/备注") + @PreAuthorize("hasAuthority('cms:contactLead:update')") + @PutMapping() + public ApiResult update(@RequestBody CmsContactLead lead) { + if (cmsContactLeadService.updateById(lead)) { + return success("更新成功"); + } + return fail("更新失败"); + } + + @Operation(summary = "删除线索") + @PreAuthorize("hasAuthority('cms:contactLead:remove')") + @DeleteMapping("/{id}") + public ApiResult remove(@PathVariable("id") Integer id) { + if (cmsContactLeadService.removeById(id)) { + return success("删除成功"); + } + return fail("删除失败"); + } + + @Operation(summary = "批量修改") + @PreAuthorize("hasAuthority('cms:contactLead:update')") + @PutMapping("/batch") + public ApiResult updateBatch(@RequestBody BatchParam batchParam) { + if (batchParam.update(cmsContactLeadService, "lead_id")) { + return success("批量修改成功"); + } + return fail("批量修改失败"); + } + + @Operation(summary = "批量删除") + @PreAuthorize("hasAuthority('cms:contactLead:remove')") + @DeleteMapping("/batch") + public ApiResult removeBatch(@RequestBody List ids) { + if (cmsContactLeadService.removeByIds(ids)) { + return success("删除成功"); + } + return fail("删除失败"); + } + + // ------------------------- + // 工具方法 + // ------------------------- + + /** + * 获取客户端真实IP(兼容反向代理) + */ + private String getClientIp(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + // X-Forwarded-For 多个IP取第一个 + if (ip != null && ip.contains(",")) { + ip = ip.split(",")[0].trim(); + } + return ip; + } +} diff --git a/src/main/java/com/gxwebsoft/cms/entity/CmsContactLead.java b/src/main/java/com/gxwebsoft/cms/entity/CmsContactLead.java new file mode 100644 index 0000000..7e6f604 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/entity/CmsContactLead.java @@ -0,0 +1,83 @@ +package com.gxwebsoft.cms.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 联系表单/销售线索 + * + * @author 科技小王子 + * @since 2026-03-30 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(name = "CmsContactLead对象", description = "联系表单/销售线索") +public class CmsContactLead implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "线索ID") + @TableId(value = "lead_id", type = IdType.AUTO) + private Integer leadId; + + @Schema(description = "联系人姓名") + private String name; + + @Schema(description = "手机号") + private String phone; + + @Schema(description = "单位/公司名称") + private String company; + + @Schema(description = "交付方式: saas/private/hybrid") + private String delivery; + + @Schema(description = "需求描述") + private String need; + + @Schema(description = "来源渠道: web/miniapp/app") + private String source; + + @Schema(description = "提交IP") + private String ip; + + @Schema(description = "跟进状态: 0待跟进 1跟进中 2已成交 3无效") + private Integer status; + + @Schema(description = "销售备注") + private String remarks; + + @Schema(description = "是否删除, 0否, 1是") + @TableLogic + private Integer deleted; + + @Schema(description = "租户ID") + private Integer tenantId; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + + public String getStatusText() { + if (this.status == null) return ""; + return switch (this.status) { + case 0 -> "待跟进"; + case 1 -> "跟进中"; + case 2 -> "已成交"; + case 3 -> "无效"; + default -> ""; + }; + } +} diff --git a/src/main/java/com/gxwebsoft/cms/mapper/CmsContactLeadMapper.java b/src/main/java/com/gxwebsoft/cms/mapper/CmsContactLeadMapper.java new file mode 100644 index 0000000..d8cc641 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/mapper/CmsContactLeadMapper.java @@ -0,0 +1,36 @@ +package com.gxwebsoft.cms.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.cms.entity.CmsContactLead; +import com.gxwebsoft.cms.param.CmsContactLeadParam; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 联系表单/销售线索Mapper + * + * @author 科技小王子 + * @since 2026-03-30 + */ +public interface CmsContactLeadMapper extends BaseMapper { + + /** + * 分页查询 + * + * @param page 分页对象 + * @param param 查询参数 + * @return List + */ + List selectPageRel(@Param("page") IPage page, + @Param("param") CmsContactLeadParam param); + + /** + * 查询全部 + * + * @param param 查询参数 + * @return List + */ + List selectListRel(@Param("param") CmsContactLeadParam param); +} diff --git a/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsContactLeadMapper.xml b/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsContactLeadMapper.xml new file mode 100644 index 0000000..d1fabdf --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsContactLeadMapper.xml @@ -0,0 +1,66 @@ + + + + + + + SELECT a.* + FROM cms_contact_lead a + + + AND a.lead_id = #{param.leadId} + + + AND a.name LIKE CONCAT('%', #{param.name}, '%') + + + AND a.phone LIKE CONCAT('%', #{param.phone}, '%') + + + AND a.company LIKE CONCAT('%', #{param.company}, '%') + + + AND a.delivery = #{param.delivery} + + + AND a.source = #{param.source} + + + AND a.status = #{param.status} + + + AND ( + a.name LIKE CONCAT('%', #{param.keywords}, '%') + OR a.phone LIKE CONCAT('%', #{param.keywords}, '%') + OR a.company LIKE CONCAT('%', #{param.keywords}, '%') + OR a.need LIKE CONCAT('%', #{param.keywords}, '%') + ) + + + AND a.deleted = #{param.deleted} + + + AND a.deleted = 0 + + + AND a.create_time >= #{param.createTimeStart} + + + AND a.create_time <= #{param.createTimeEnd} + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/cms/param/CmsContactLeadParam.java b/src/main/java/com/gxwebsoft/cms/param/CmsContactLeadParam.java new file mode 100644 index 0000000..4855dc9 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/param/CmsContactLeadParam.java @@ -0,0 +1,56 @@ +package com.gxwebsoft.cms.param; + +import com.gxwebsoft.common.core.annotation.QueryField; +import com.gxwebsoft.common.core.annotation.QueryType; +import com.gxwebsoft.common.core.web.BaseParam; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 联系表单/销售线索查询参数 + * + * @author 科技小王子 + * @since 2026-03-30 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(name = "CmsContactLeadParam对象", description = "联系表单/销售线索查询参数") +public class CmsContactLeadParam extends BaseParam { + + private static final long serialVersionUID = 1L; + + @Schema(description = "线索ID") + @QueryField(type = QueryType.EQ) + private Integer leadId; + + @Schema(description = "联系人姓名") + @QueryField(type = QueryType.LIKE) + private String name; + + @Schema(description = "手机号") + @QueryField(type = QueryType.LIKE) + private String phone; + + @Schema(description = "单位/公司名称") + @QueryField(type = QueryType.LIKE) + private String company; + + @Schema(description = "交付方式: saas/private/hybrid") + @QueryField(type = QueryType.EQ) + private String delivery; + + @Schema(description = "来源渠道") + @QueryField(type = QueryType.EQ) + private String source; + + @Schema(description = "跟进状态: 0待跟进 1跟进中 2已成交 3无效") + @QueryField(type = QueryType.EQ) + private Integer status; + + @Schema(description = "是否删除, 0否, 1是") + @QueryField(type = QueryType.EQ) + private Integer deleted; +} diff --git a/src/main/java/com/gxwebsoft/cms/service/CmsContactLeadService.java b/src/main/java/com/gxwebsoft/cms/service/CmsContactLeadService.java new file mode 100644 index 0000000..e1c5e02 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/service/CmsContactLeadService.java @@ -0,0 +1,42 @@ +package com.gxwebsoft.cms.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.cms.entity.CmsContactLead; +import com.gxwebsoft.cms.param.CmsContactLeadParam; +import com.gxwebsoft.common.core.web.PageResult; + +import java.util.List; + +/** + * 联系表单/销售线索Service + * + * @author 科技小王子 + * @since 2026-03-30 + */ +public interface CmsContactLeadService extends IService { + + /** + * 分页关联查询 + * + * @param param 查询参数 + * @return PageResult + */ + PageResult pageRel(CmsContactLeadParam param); + + /** + * 关联查询全部 + * + * @param param 查询参数 + * @return List + */ + List listRel(CmsContactLeadParam param); + + /** + * 提交联系表单(不需要登录) + * + * @param lead 表单数据 + * @param ip 客户端IP + * @return boolean + */ + boolean submit(CmsContactLead lead, String ip); +} diff --git a/src/main/java/com/gxwebsoft/cms/service/impl/CmsContactLeadServiceImpl.java b/src/main/java/com/gxwebsoft/cms/service/impl/CmsContactLeadServiceImpl.java new file mode 100644 index 0000000..6fe172e --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/service/impl/CmsContactLeadServiceImpl.java @@ -0,0 +1,209 @@ +package com.gxwebsoft.cms.service.impl; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.cms.entity.CmsContactLead; +import com.gxwebsoft.cms.mapper.CmsContactLeadMapper; +import com.gxwebsoft.cms.param.CmsContactLeadParam; +import com.gxwebsoft.cms.service.CmsContactLeadService; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 联系表单/销售线索Service实现 + * + * @author 科技小王子 + * @since 2026-03-30 + */ +@Slf4j +@Service +public class CmsContactLeadServiceImpl extends ServiceImpl + implements CmsContactLeadService { + + /** 企业微信群机器人 Webhook,留空则不发送 */ + @Value("${notify.wecom-webhook:}") + private String wecomWebhook; + + /** 飞书群机器人 Webhook,留空则不发送 */ + @Value("${notify.feishu-webhook:}") + private String feishuWebhook; + + // -------------------------------------------------------- + // 分页 / 列表 + // -------------------------------------------------------- + + @Override + public PageResult pageRel(CmsContactLeadParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("create_time desc"); + List list = baseMapper.selectPageRel(page, param); + return new PageResult<>(list, page.getTotal()); + } + + @Override + public List listRel(CmsContactLeadParam param) { + return baseMapper.selectListRel(param); + } + + // -------------------------------------------------------- + // 公开提交接口 + // -------------------------------------------------------- + + @Override + public boolean submit(CmsContactLead lead, String ip) { + // 补充默认值 + if (StrUtil.isBlank(lead.getSource())) { + lead.setSource("web"); + } + lead.setIp(ip); + lead.setStatus(0); // 默认待跟进 + + boolean result = save(lead); + if (result) { + log.info("[联系表单] 新线索提交成功,name={}, phone={}", lead.getName(), lead.getPhone()); + // 异步推送通知,不阻塞接口响应 + sendNotifyAsync(lead); + } + return result; + } + + // -------------------------------------------------------- + // 异步通知(企业微信 + 飞书) + // -------------------------------------------------------- + + @Async + public void sendNotifyAsync(CmsContactLead lead) { + String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("MM-dd HH:mm")); + String deliveryLabel = deliveryLabel(lead.getDelivery()); + + if (StrUtil.isNotBlank(wecomWebhook)) { + try { + sendWecom(lead, deliveryLabel, now); + } catch (Exception e) { + log.warn("[联系表单] 企业微信通知发送失败: {}", e.getMessage()); + } + } + + if (StrUtil.isNotBlank(feishuWebhook)) { + try { + sendFeishu(lead, deliveryLabel, now); + } catch (Exception e) { + log.warn("[联系表单] 飞书通知发送失败: {}", e.getMessage()); + } + } + } + + /** + * 企业微信群机器人 - markdown 消息 + * 文档:https://developer.work.weixin.qq.com/document/path/91770 + */ + private void sendWecom(CmsContactLead lead, String deliveryLabel, String now) { + String content = String.format( + "## 📋 新销售线索(%s)\n" + + "> **姓名:** %s\n" + + "> **手机:** %s\n" + + "> **公司:** %s\n" + + "> **交付:** %s\n" + + "> **需求:** %s\n" + + "> **来源:** %s", + now, + nullSafe(lead.getName()), + nullSafe(lead.getPhone()), + nullSafe(lead.getCompany()), + deliveryLabel, + truncate(lead.getNeed(), 100), + nullSafe(lead.getSource()) + ); + + Map textMap = new HashMap<>(); + textMap.put("content", content); + + Map payload = new HashMap<>(); + payload.put("msgtype", "markdown"); + payload.put("markdown", textMap); + + String body = JSON.toJSONString(payload); + String resp = HttpUtil.post(wecomWebhook, body); + log.info("[联系表单] 企业微信通知结果: {}", resp); + } + + /** + * 飞书群机器人 - 富文本(post)消息 + * 文档:https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot + */ + private void sendFeishu(CmsContactLead lead, String deliveryLabel, String now) { + // 标题行 + String title = "📋 新销售线索(" + now + ")"; + + // 正文每行:[{"tag":"text","text":"xxx"}] + List>> content = List.of( + line("姓 名:" + nullSafe(lead.getName())), + line("手 机:" + nullSafe(lead.getPhone())), + line("公 司:" + nullSafe(lead.getCompany())), + line("交 付:" + deliveryLabel), + line("需 求:" + truncate(lead.getNeed(), 100)), + line("来 源:" + nullSafe(lead.getSource())) + ); + + Map zhCn = new HashMap<>(); + zhCn.put("title", title); + zhCn.put("content", content); + + Map postContent = new HashMap<>(); + postContent.put("zh_cn", zhCn); + + Map post = new HashMap<>(); + post.put("content", postContent); + + Map payload = new HashMap<>(); + payload.put("msg_type", "post"); + payload.put("content", post); + + String body = JSON.toJSONString(payload); + String resp = HttpUtil.post(feishuWebhook, body); + log.info("[联系表单] 飞书通知结果: {}", resp); + } + + // -------------------------------------------------------- + // 工具方法 + // -------------------------------------------------------- + + private String deliveryLabel(String delivery) { + if (delivery == null) return "未选择"; + return switch (delivery) { + case "saas" -> "SaaS(云端)"; + case "private" -> "私有化部署"; + case "hybrid" -> "混合部署"; + default -> delivery; + }; + } + + private String nullSafe(String s) { + return StrUtil.isBlank(s) ? "—" : s; + } + + private String truncate(String s, int max) { + if (StrUtil.isBlank(s)) return "—"; + return s.length() > max ? s.substring(0, max) + "…" : s; + } + + /** 飞书 post 消息单行 */ + private List> line(String text) { + Map node = new HashMap<>(); + node.put("tag", "text"); + node.put("text", text); + return List.of(node); + } +} diff --git a/src/main/java/com/gxwebsoft/cms/sql/cms_contact_lead.sql b/src/main/java/com/gxwebsoft/cms/sql/cms_contact_lead.sql new file mode 100644 index 0000000..45d6154 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/sql/cms_contact_lead.sql @@ -0,0 +1,25 @@ +-- ---------------------------- +-- 联系表单/销售线索表 +-- 存放官网 /contact 页面提交的咨询需求 +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `cms_contact_lead` ( + `lead_id` INT NOT NULL AUTO_INCREMENT COMMENT '线索ID', + `name` VARCHAR(50) NOT NULL COMMENT '联系人姓名', + `phone` VARCHAR(20) NOT NULL COMMENT '手机号', + `company` VARCHAR(100) DEFAULT NULL COMMENT '单位/公司名称', + `delivery` VARCHAR(20) DEFAULT NULL COMMENT '交付方式: saas/private/hybrid', + `need` TEXT DEFAULT NULL COMMENT '需求描述', + `source` VARCHAR(50) DEFAULT 'web' COMMENT '来源渠道: web/miniapp/app', + `ip` VARCHAR(50) DEFAULT NULL COMMENT '提交IP', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '跟进状态: 0待跟进 1跟进中 2已成交 3无效', + `remarks` VARCHAR(500) DEFAULT NULL COMMENT '销售备注', + `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是', + `tenant_id` INT DEFAULT NULL COMMENT '租户ID', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`lead_id`), + KEY `idx_tenant_id` (`tenant_id`), + KEY `idx_phone` (`phone`), + KEY `idx_status` (`status`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='联系表单/销售线索'; diff --git a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java index 8cd95a0..c9d1463 100644 --- a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java +++ b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java @@ -80,7 +80,8 @@ public class SecurityConfig { "/api/shop/getShopInfo", "/api/shop/shop-order/test", "/api/qr-code/**", - "/api/shop/order-delivery/notify" + "/api/shop/order-delivery/notify", + "/api/cms/cms-contact-lead/submit" ) .permitAll() .anyRequest() diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ada4ffe..10fe6e9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -258,9 +258,12 @@ payment: # 开发环境是否启用环境感知 environment-aware: true - # 生产环境配置 - prod: - # 生产环境回调地址 - notify-url: "https://cms-api.websoft.top/api/shop/shop-order/notify" - # 生产环境是否启用环境感知 - environment-aware: false + +# 通知配置(企业微信/飞书机器人 Webhook) +notify: + # 企业微信群机器人 Webhook,格式:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY + # 不启用时留空或删除此行 + wecom-webhook: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=aa0d2f30-b785-44a2-ad19-05834569b7c5" + # 飞书群机器人 Webhook,格式:https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_TOKEN + feishu-webhook: "" + From 424929222f9086c0940d1a731eaaa3e8f745f3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 30 Mar 2026 11:30:29 +0800 Subject: [PATCH 08/21] =?UTF-8?q?feat(contact):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=81=94=E7=B3=BB=E8=A1=A8=E5=8D=95=E9=94=80=E5=94=AE=E7=BA=BF?= =?UTF-8?q?=E7=B4=A2=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增联系表单数据库表结构设计与实体类定义 - 实现联系表单提交接口,支持公开访问无需登录验证 - 添加后台管理接口,支持线索查询、状态更新和删除操作 - 集成企业微信和飞书机器人通知功能,实时推送新线索 - 在安全配置中开放联系表单提交接口访问权限 - 添加应用配置中的通知机器人Webhook配置项 --- .../gxwebsoft/app/controller/AppUserController.java | 13 ++++++++++++- .../com/gxwebsoft/app/service/AppUserService.java | 8 ++++++++ .../app/service/impl/AppUserServiceImpl.java | 5 +++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gxwebsoft/app/controller/AppUserController.java b/src/main/java/com/gxwebsoft/app/controller/AppUserController.java index 4124862..695369c 100644 --- a/src/main/java/com/gxwebsoft/app/controller/AppUserController.java +++ b/src/main/java/com/gxwebsoft/app/controller/AppUserController.java @@ -67,13 +67,24 @@ public class AppUserController extends BaseController { return fail("添加失败"); } - @Operation(summary = "邀请用户成为应用成员") + @Operation(summary = "邀请用户成为应用成员(支持用户ID或手机号)") @PostMapping("/invite") public ApiResult invite(@RequestBody AppUser appUser) { User loginUser = getLoginUser(); if (loginUser == null) { return fail("请先登录"); } + // 支持手机号邀请:若 userId 为空但传了 phone,则先按手机号查出用户 + if (appUser.getUserId() == null && appUser.getPhone() != null && !appUser.getPhone().isEmpty()) { + User targetUser = appUserService.findUserByPhone(appUser.getPhone()); + if (targetUser == null) { + return fail("手机号未注册,请确认后再试"); + } + appUser.setUserId(targetUser.getUserId()); + } + if (appUser.getUserId() == null) { + return fail("请输入用户ID或手机号"); + } try { AppUser result = appUserService.inviteUser( appUser.getWebsiteId(), diff --git a/src/main/java/com/gxwebsoft/app/service/AppUserService.java b/src/main/java/com/gxwebsoft/app/service/AppUserService.java index 6888fc2..0aa0793 100644 --- a/src/main/java/com/gxwebsoft/app/service/AppUserService.java +++ b/src/main/java/com/gxwebsoft/app/service/AppUserService.java @@ -57,4 +57,12 @@ public interface AppUserService extends IService { */ boolean isMember(Long websiteId, Integer userId); + /** + * 根据手机号查找系统用户(用于手机号邀请) + * + * @param phone 手机号 + * @return 系统用户,不存在返回 null + */ + com.gxwebsoft.common.system.entity.User findUserByPhone(String phone); + } diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java index 0732a7d..9d38689 100644 --- a/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppUserServiceImpl.java @@ -104,4 +104,9 @@ public class AppUserServiceImpl extends ServiceImpl impl return count > 0; } + @Override + public User findUserByPhone(String phone) { + return userService.getByPhone(phone); + } + } From 607589d2c5f2d8ed94ffbb311e096cb41b2f0a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 30 Mar 2026 11:39:23 +0800 Subject: [PATCH 09/21] =?UTF-8?q?feat(user):=20=E6=B7=BB=E5=8A=A0=E6=A0=B9?= =?UTF-8?q?=E6=8D=AE=E6=89=8B=E6=9C=BA=E5=8F=B7=E6=9F=A5=E8=AF=A2=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=9A=84=E8=B7=A8=E7=A7=9F=E6=88=B7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 UserMapper 中新增 selectByPhone 方法,支持忽略租户隔离查询 - 更新 UserMapper.xml 中的 selectByPhone 查询语句,实现跨库查询逻辑 - 修改 UserServiceImpl 中的 getByPhone 方法,使用自定义 SQL 避免租户拦截器影响 - 实现跨租户查询,直接访问 gxwebsoft_core.sys_user 表获取用户信息 --- .../gxwebsoft/common/system/mapper/UserMapper.java | 8 ++++++++ .../common/system/mapper/xml/UserMapper.xml | 13 +++---------- .../common/system/service/impl/UserServiceImpl.java | 3 ++- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java b/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java index 41c8be0..a5c4f35 100644 --- a/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java +++ b/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java @@ -62,6 +62,14 @@ public interface UserMapper extends BaseMapper { @InterceptorIgnore(tenantLine = "true") User selectByIdIgnoreTenant(@Param("userId") Integer userId); + /** + * 根据手机号查询用户(忽略租户隔离,跨库查 gxwebsoft_core.sys_user) + * @param phone 手机号 + * @return User + */ + @InterceptorIgnore(tenantLine = "true") + User selectByPhone(@Param("phone") String phone); + @InterceptorIgnore(tenantLine = "true") List pageAdminByPhone(@Param("param") UserParam param); diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml b/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml index df27bef..4a2a32e 100644 --- a/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml +++ b/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml @@ -182,20 +182,13 @@
- + diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java index b321832..6ba5309 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java @@ -180,7 +180,8 @@ public class UserServiceImpl extends ServiceImpl implements Us @Override public User getByPhone(String phone) { - return query().eq("phone", phone).one(); + // 使用自定义 SQL(@InterceptorIgnore),避免 TenantLineInterceptor 把表定位到当前库的 sys_user + return baseMapper.selectByPhone(phone); } @Override From a4d5c861343d01b464524d5bacabb63e603c8fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 30 Mar 2026 11:59:41 +0800 Subject: [PATCH 10/21] =?UTF-8?q?feat(app):=20=E6=B7=BB=E5=8A=A0=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=B7=A5=E5=8D=95=E7=B3=BB=E7=BB=9F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建AppTicket实体类定义工单数据结构 - 实现AppTicketController提供工单CRUD接口 - 创建AppTicketReply实体支持工单回复功能 - 实现工单提交、查询、状态更新等核心业务逻辑 - 添加工单统计、分配、回复等功能接口 - 集成MyBatis Plus实现数据库操作 - 支持工单分类、优先级、状态流转管理 - 实现自动分配工单给技术成员机制 - 添加工单搜索、分页、权限控制功能 --- .../app/controller/AppTicketController.java | 150 +++++++++ .../com/gxwebsoft/app/entity/AppTicket.java | 97 ++++++ .../gxwebsoft/app/entity/AppTicketReply.java | 58 ++++ .../gxwebsoft/app/mapper/AppTicketMapper.java | 11 + .../app/mapper/AppTicketReplyMapper.java | 10 + .../gxwebsoft/app/param/AppTicketParam.java | 33 ++ .../app/service/AppTicketService.java | 46 +++ .../service/impl/AppTicketServiceImpl.java | 303 ++++++++++++++++++ 8 files changed, 708 insertions(+) create mode 100644 src/main/java/com/gxwebsoft/app/controller/AppTicketController.java create mode 100644 src/main/java/com/gxwebsoft/app/entity/AppTicket.java create mode 100644 src/main/java/com/gxwebsoft/app/entity/AppTicketReply.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/AppTicketMapper.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/AppTicketReplyMapper.java create mode 100644 src/main/java/com/gxwebsoft/app/param/AppTicketParam.java create mode 100644 src/main/java/com/gxwebsoft/app/service/AppTicketService.java create mode 100644 src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java diff --git a/src/main/java/com/gxwebsoft/app/controller/AppTicketController.java b/src/main/java/com/gxwebsoft/app/controller/AppTicketController.java new file mode 100644 index 0000000..68c718b --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/controller/AppTicketController.java @@ -0,0 +1,150 @@ +package com.gxwebsoft.app.controller; + +import com.gxwebsoft.app.entity.AppTicket; +import com.gxwebsoft.app.entity.AppTicketReply; +import com.gxwebsoft.app.param.AppTicketParam; +import com.gxwebsoft.app.service.AppTicketService; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.system.entity.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * 应用工单控制器 + * + * @author 科技小王子 + * @since 2026-03-30 + */ +@Slf4j +@Tag(name = "应用工单管理") +@RestController +@RequestMapping("/api/app/ticket") +public class AppTicketController extends BaseController { + + @Resource + private AppTicketService appTicketService; + + // ─── 客户端接口 ──────────────────────────────────────────────── + + @Operation(summary = "查询我的工单(分页)") + @GetMapping("/my") + public ApiResult> myTickets(AppTicketParam param) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + return success(appTicketService.myPage(param, loginUser.getUserId())); + } + + @Operation(summary = "提交工单") + @PostMapping("/submit") + public ApiResult submit(@RequestBody AppTicket ticket) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + try { + AppTicket result = appTicketService.submit(ticket, loginUser.getUserId()); + return success("工单提交成功", result); + } catch (Exception e) { + return fail(e.getMessage()); + } + } + + @Operation(summary = "关闭工单(提交人)") + @PutMapping("/{ticketId}/close") + public ApiResult close(@PathVariable Long ticketId) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + try { + appTicketService.closeByUser(ticketId, loginUser.getUserId()); + return success("工单已关闭"); + } catch (Exception e) { + return fail(e.getMessage()); + } + } + + // ─── 技术端接口 ──────────────────────────────────────────────── + + @Operation(summary = "查询所有工单(技术人员)") + @GetMapping("/list") + public ApiResult> allTickets(AppTicketParam param) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + return success(appTicketService.allPage(param)); + } + + @Operation(summary = "获取工单详情") + @GetMapping("/{ticketId}") + public ApiResult detail(@PathVariable Long ticketId) { + return success(appTicketService.getById(ticketId)); + } + + @Operation(summary = "更新工单状态(技术人员)") + @PutMapping("/status") + public ApiResult updateStatus(@RequestBody Map body) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + Long ticketId = Long.valueOf(body.get("ticketId").toString()); + String status = body.get("status").toString(); + appTicketService.updateStatus(ticketId, status, loginUser.getUserId()); + return success("状态已更新"); + } + + @Operation(summary = "分配处理人(管理员)") + @PutMapping("/assign") + public ApiResult assign(@RequestBody Map body) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + Long ticketId = Long.valueOf(body.get("ticketId").toString()); + Integer assigneeId = Integer.valueOf(body.get("assigneeId").toString()); + appTicketService.assign(ticketId, assigneeId); + return success("分配成功"); + } + + // ─── 回复接口 ───────────────────────────────────────────────── + + @Operation(summary = "获取工单回复列表") + @GetMapping("/{ticketId}/replies") + public ApiResult> replies(@PathVariable Long ticketId) { + return success(appTicketService.getReplies(ticketId)); + } + + @Operation(summary = "提交工单回复") + @PostMapping("/reply") + public ApiResult reply(@RequestBody AppTicketReply reply) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + if (reply.getContent() == null || reply.getContent().trim().isEmpty()) { + return fail("回复内容不能为空"); + } + try { + AppTicketReply result = appTicketService.addReply(reply, loginUser.getUserId()); + return success("回复成功", result); + } catch (Exception e) { + return fail(e.getMessage()); + } + } + + // ─── 统计 & 辅助 ───────────────────────────────────────────── + + @Operation(summary = "工单统计数据") + @GetMapping("/stats") + public ApiResult> stats( + @RequestParam(required = false) Long websiteId) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + // 技术端不限制用户维度;客户端通过路由区分 + return success(appTicketService.stats(websiteId, null)); + } + + @Operation(summary = "获取技术人员列表(用于分配)") + @GetMapping("/staff-list") + public ApiResult>> staffList() { + return success(appTicketService.getTechStaffList()); + } +} diff --git a/src/main/java/com/gxwebsoft/app/entity/AppTicket.java b/src/main/java/com/gxwebsoft/app/entity/AppTicket.java new file mode 100644 index 0000000..1d62783 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/entity/AppTicket.java @@ -0,0 +1,97 @@ +package com.gxwebsoft.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 应用工单 + * + * @author 科技小王子 + * @since 2026-03-30 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("app_ticket") +@Schema(name = "AppTicket对象", description = "应用工单") +public class AppTicket implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "工单ID") + @TableId(value = "ticket_id", type = IdType.AUTO) + private Long ticketId; + + @Schema(description = "工单编号(TK-yyyyMMddHHmmss+4位随机)") + private String ticketNo; + + @Schema(description = "工单标题") + private String title; + + @Schema(description = "工单内容描述") + private String content; + + @Schema(description = "关联应用ID") + private Long websiteId; + + @Schema(description = "应用名称(冗余)") + private String websiteName; + + @Schema(description = "工单分类: bug/feature/consultation/complaint/other") + private String category; + + @Schema(description = "优先级: low/normal/high/urgent") + private String priority; + + @Schema(description = "状态: pending/assigned/processing/resolved/closed/rejected") + private String status; + + @Schema(description = "附件JSON数组") + private String attachments; + + @Schema(description = "提交人用户ID") + private Integer submitUserId; + + @Schema(description = "提交人昵称(冗余)") + private String submitUserName; + + @Schema(description = "提交人头像(冗余)") + private String submitUserAvatar; + + @Schema(description = "分配的处理人用户ID") + private Integer assigneeId; + + @Schema(description = "处理人昵称(冗余)") + private String assigneeName; + + @Schema(description = "处理人头像(冗余)") + private String assigneeAvatar; + + @Schema(description = "回复数量") + private Integer replyCount; + + @Schema(description = "解决时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime resolvedTime; + + @Schema(description = "关闭时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime closedTime; + + @Schema(description = "是否删除: 0否 1是") + private Integer deleted; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/gxwebsoft/app/entity/AppTicketReply.java b/src/main/java/com/gxwebsoft/app/entity/AppTicketReply.java new file mode 100644 index 0000000..fb13ca9 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/entity/AppTicketReply.java @@ -0,0 +1,58 @@ +package com.gxwebsoft.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 工单回复 + * + * @author 科技小王子 + * @since 2026-03-30 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("app_ticket_reply") +@Schema(name = "AppTicketReply对象", description = "工单回复") +public class AppTicketReply implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "回复ID") + @TableId(value = "reply_id", type = IdType.AUTO) + private Long replyId; + + @Schema(description = "关联工单ID") + private Long ticketId; + + @Schema(description = "回复内容") + private String content; + + @Schema(description = "附件JSON数组") + private String attachments; + + @Schema(description = "回复人用户ID") + private Integer userId; + + @Schema(description = "回复人昵称(冗余)") + private String userName; + + @Schema(description = "回复人头像(冗余)") + private String userAvatar; + + @Schema(description = "是否是技术人员/客服: 0否 1是") + private Integer isStaff; + + @Schema(description = "是否删除: 0否 1是") + private Integer deleted; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/AppTicketMapper.java b/src/main/java/com/gxwebsoft/app/mapper/AppTicketMapper.java new file mode 100644 index 0000000..63e30e8 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/AppTicketMapper.java @@ -0,0 +1,11 @@ +package com.gxwebsoft.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.gxwebsoft.app.entity.AppTicket; +import org.apache.ibatis.annotations.Param; + +/** + * 应用工单 Mapper + */ +public interface AppTicketMapper extends BaseMapper { +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/AppTicketReplyMapper.java b/src/main/java/com/gxwebsoft/app/mapper/AppTicketReplyMapper.java new file mode 100644 index 0000000..3fc0a16 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/AppTicketReplyMapper.java @@ -0,0 +1,10 @@ +package com.gxwebsoft.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.gxwebsoft.app.entity.AppTicketReply; + +/** + * 工单回复 Mapper + */ +public interface AppTicketReplyMapper extends BaseMapper { +} diff --git a/src/main/java/com/gxwebsoft/app/param/AppTicketParam.java b/src/main/java/com/gxwebsoft/app/param/AppTicketParam.java new file mode 100644 index 0000000..a10144c --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/param/AppTicketParam.java @@ -0,0 +1,33 @@ +package com.gxwebsoft.app.param; + +import com.gxwebsoft.common.core.web.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 工单查询参数 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "工单查询参数") +public class AppTicketParam extends PageParam { + + @Schema(description = "关联应用ID") + private Long websiteId; + + @Schema(description = "工单状态") + private String status; + + @Schema(description = "工单分类") + private String category; + + @Schema(description = "优先级") + private String priority; + + @Schema(description = "处理人ID(传0=未分配)") + private Integer assigneeId; + + @Schema(description = "关键词(标题/工单编号)") + private String keywords; +} diff --git a/src/main/java/com/gxwebsoft/app/service/AppTicketService.java b/src/main/java/com/gxwebsoft/app/service/AppTicketService.java new file mode 100644 index 0000000..d81bf6e --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/AppTicketService.java @@ -0,0 +1,46 @@ +package com.gxwebsoft.app.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.app.entity.AppTicket; +import com.gxwebsoft.app.entity.AppTicketReply; +import com.gxwebsoft.app.param.AppTicketParam; +import com.gxwebsoft.common.core.web.PageResult; + +import java.util.List; +import java.util.Map; + +/** + * 应用工单 Service + */ +public interface AppTicketService extends IService { + + /** 客户端:查自己提交的工单(分页) */ + PageResult myPage(AppTicketParam param, Integer userId); + + /** 技术端:查询所有工单(分页) */ + PageResult allPage(AppTicketParam param); + + /** 提交工单(自动分配) */ + AppTicket submit(AppTicket ticket, Integer userId); + + /** 更新状态(技术人员操作) */ + void updateStatus(Long ticketId, String status, Integer operatorId); + + /** 分配处理人 */ + void assign(Long ticketId, Integer assigneeId); + + /** 关闭工单(提交人操作) */ + void closeByUser(Long ticketId, Integer userId); + + /** 获取工单回复列表 */ + List getReplies(Long ticketId); + + /** 添加回复 */ + AppTicketReply addReply(AppTicketReply reply, Integer userId); + + /** 统计数据 */ + Map stats(Long websiteId, Integer userId); + + /** 获取技术人员列表(角色:developer/admin 的应用成员) */ + List> getTechStaffList(); +} diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java new file mode 100644 index 0000000..dd98f05 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java @@ -0,0 +1,303 @@ +package com.gxwebsoft.app.service.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.app.entity.AppTicket; +import com.gxwebsoft.app.entity.AppTicketReply; +import com.gxwebsoft.app.entity.AppUser; +import com.gxwebsoft.app.mapper.AppTicketMapper; +import com.gxwebsoft.app.mapper.AppTicketReplyMapper; +import com.gxwebsoft.app.param.AppTicketParam; +import com.gxwebsoft.app.service.AppTicketService; +import com.gxwebsoft.app.service.AppUserService; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.common.system.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 应用工单 Service 实现 + * + * @author 科技小王子 + * @since 2026-03-30 + */ +@Slf4j +@Service +public class AppTicketServiceImpl extends ServiceImpl implements AppTicketService { + + @Resource + private AppTicketReplyMapper replyMapper; + + @Resource + private AppUserService appUserService; + + @Resource + private UserService userService; + + // ─── 生成工单编号 ───────────────────────────────────────────── + private String generateTicketNo() { + String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + String rand = String.format("%04d", new Random().nextInt(10000)); + return "TK-" + ts + rand; + } + + // ─── 客户端:查自己的工单 ───────────────────────────────────── + @Override + public PageResult myPage(AppTicketParam param, Integer userId) { + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = + new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>( + param.getPageNum() != null ? param.getPageNum() : 1, + param.getPageSize() != null ? param.getPageSize() : 15); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(AppTicket::getDeleted, 0) + .eq(AppTicket::getSubmitUserId, userId) + .eq(ObjectUtil.isNotNull(param.getWebsiteId()), AppTicket::getWebsiteId, param.getWebsiteId()) + .eq(ObjectUtil.isNotEmpty(param.getStatus()), AppTicket::getStatus, param.getStatus()) + .eq(ObjectUtil.isNotEmpty(param.getCategory()), AppTicket::getCategory, param.getCategory()) + .and(ObjectUtil.isNotEmpty(param.getKeywords()), q -> + q.like(AppTicket::getTitle, param.getKeywords()) + .or().like(AppTicket::getTicketNo, param.getKeywords())) + .orderByDesc(AppTicket::getCreateTime); + + baseMapper.selectPage(page, wrapper); + return new PageResult<>(page.getRecords(), page.getTotal()); + } + + // ─── 技术端:查所有工单 ─────────────────────────────────────── + @Override + public PageResult allPage(AppTicketParam param) { + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = + new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>( + param.getPageNum() != null ? param.getPageNum() : 1, + param.getPageSize() != null ? param.getPageSize() : 20); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(AppTicket::getDeleted, 0) + .eq(ObjectUtil.isNotNull(param.getWebsiteId()), AppTicket::getWebsiteId, param.getWebsiteId()) + .eq(ObjectUtil.isNotEmpty(param.getStatus()), AppTicket::getStatus, param.getStatus()) + .eq(ObjectUtil.isNotEmpty(param.getCategory()), AppTicket::getCategory, param.getCategory()) + .eq(ObjectUtil.isNotEmpty(param.getPriority()), AppTicket::getPriority, param.getPriority()) + .and(ObjectUtil.isNotNull(param.getAssigneeId()) && param.getAssigneeId() == 0, + q -> q.isNull(AppTicket::getAssigneeId)) + .eq(ObjectUtil.isNotNull(param.getAssigneeId()) && param.getAssigneeId() > 0, + AppTicket::getAssigneeId, param.getAssigneeId()) + .and(ObjectUtil.isNotEmpty(param.getKeywords()), q -> + q.like(AppTicket::getTitle, param.getKeywords()) + .or().like(AppTicket::getTicketNo, param.getKeywords())) + .orderByAsc(AppTicket::getStatus) // pending 排前面 + .orderByDesc(AppTicket::getCreateTime); + + baseMapper.selectPage(page, wrapper); + return new PageResult<>(page.getRecords(), page.getTotal()); + } + + // ─── 提交工单(自动分配) ───────────────────────────────────── + @Override + @Transactional(rollbackFor = Exception.class) + public AppTicket submit(AppTicket ticket, Integer userId) { + // 补充提交人信息 + ticket.setSubmitUserId(userId); + ticket.setTicketNo(generateTicketNo()); + ticket.setStatus("pending"); + ticket.setReplyCount(0); + ticket.setDeleted(0); + ticket.setCreateTime(LocalDateTime.now()); + ticket.setUpdateTime(LocalDateTime.now()); + + // 从用户服务取昵称/头像冗余存储 + try { + User user = userService.getById(userId); + if (user != null) { + ticket.setSubmitUserName(user.getNickname() != null ? user.getNickname() : user.getUsername()); + ticket.setSubmitUserAvatar(user.getAvatar()); + } + } catch (Exception e) { + log.warn("获取提交人信息失败", e); + } + + // 自动分配:查找该应用的 admin/developer 成员,随机分配 + autoAssign(ticket); + + save(ticket); + return ticket; + } + + /** 自动分配工单给应用的技术成员 */ + private void autoAssign(AppTicket ticket) { + if (ticket.getWebsiteId() == null) return; + try { + List members = appUserService.list( + new LambdaQueryWrapper() + .eq(AppUser::getWebsiteId, ticket.getWebsiteId()) + .eq(AppUser::getStatus, 0) + .in(AppUser::getRole, "admin", "developer", "owner")); + if (!members.isEmpty()) { + // 简单轮询:按工单数最少分配(此处随机) + AppUser assigned = members.get(new Random().nextInt(members.size())); + ticket.setAssigneeId(assigned.getUserId()); + ticket.setAssigneeName(assigned.getNickname() != null ? assigned.getNickname() : assigned.getUsername()); + ticket.setAssigneeAvatar(assigned.getAvatar()); + ticket.setStatus("assigned"); + } + } catch (Exception e) { + log.warn("自动分配工单失败,保持 pending 状态", e); + } + } + + // ─── 更新状态 ───────────────────────────────────────────────── + @Override + public void updateStatus(Long ticketId, String status, Integer operatorId) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .eq(AppTicket::getTicketId, ticketId) + .set(AppTicket::getStatus, status) + .set(AppTicket::getUpdateTime, LocalDateTime.now()); + if ("resolved".equals(status)) { + wrapper.set(AppTicket::getResolvedTime, LocalDateTime.now()); + // 如果没有分配人,自动将操作人设为处理人 + AppTicket t = getById(ticketId); + if (t != null && t.getAssigneeId() == null) { + wrapper.set(AppTicket::getAssigneeId, operatorId); + } + } + update(wrapper); + } + + // ─── 分配处理人 ─────────────────────────────────────────────── + @Override + public void assign(Long ticketId, Integer assigneeId) { + User user = userService.getById(assigneeId); + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .eq(AppTicket::getTicketId, ticketId) + .set(AppTicket::getAssigneeId, assigneeId) + .set(user != null, AppTicket::getAssigneeName, + user != null ? (user.getNickname() != null ? user.getNickname() : user.getUsername()) : null) + .set(user != null, AppTicket::getAssigneeAvatar, user != null ? user.getAvatar() : null) + .set(AppTicket::getStatus, "assigned") + .set(AppTicket::getUpdateTime, LocalDateTime.now()); + update(wrapper); + } + + // ─── 用户关闭工单 ───────────────────────────────────────────── + @Override + public void closeByUser(Long ticketId, Integer userId) { + AppTicket ticket = getById(ticketId); + if (ticket == null || !ticket.getSubmitUserId().equals(userId)) { + throw new RuntimeException("无权操作该工单"); + } + update(new LambdaUpdateWrapper() + .eq(AppTicket::getTicketId, ticketId) + .set(AppTicket::getStatus, "closed") + .set(AppTicket::getClosedTime, LocalDateTime.now()) + .set(AppTicket::getUpdateTime, LocalDateTime.now())); + } + + // ─── 获取回复列表 ───────────────────────────────────────────── + @Override + public List getReplies(Long ticketId) { + return replyMapper.selectList( + new LambdaQueryWrapper() + .eq(AppTicketReply::getTicketId, ticketId) + .eq(AppTicketReply::getDeleted, 0) + .orderByAsc(AppTicketReply::getCreateTime)); + } + + // ─── 添加回复 ───────────────────────────────────────────────── + @Override + @Transactional(rollbackFor = Exception.class) + public AppTicketReply addReply(AppTicketReply reply, Integer userId) { + reply.setUserId(userId); + reply.setDeleted(0); + reply.setCreateTime(LocalDateTime.now()); + + // 补充用户信息 + try { + User user = userService.getById(userId); + if (user != null) { + reply.setUserName(user.getNickname() != null ? user.getNickname() : user.getUsername()); + reply.setUserAvatar(user.getAvatar()); + } + } catch (Exception e) { + log.warn("获取回复人信息失败", e); + } + + // 判断是否是技术人员(该应用的 admin/developer/owner) + AppTicket ticket = getById(reply.getTicketId()); + if (ticket != null) { + boolean isStaff = appUserService.count(new LambdaQueryWrapper() + .eq(AppUser::getWebsiteId, ticket.getWebsiteId()) + .eq(AppUser::getUserId, userId) + .in(AppUser::getRole, "owner", "admin", "developer") + .eq(AppUser::getStatus, 0)) > 0; + reply.setIsStaff(isStaff ? 1 : 0); + } else { + reply.setIsStaff(0); + } + + replyMapper.insert(reply); + + // 更新工单回复数 & 更新时间 + update(new LambdaUpdateWrapper() + .eq(AppTicket::getTicketId, reply.getTicketId()) + .setSql("reply_count = reply_count + 1") + .set(AppTicket::getUpdateTime, LocalDateTime.now()) + // 若状态是 assigned,更新为 processing + .eq(AppTicket::getStatus, "assigned") + .set(AppTicket::getStatus, "processing")); + + return reply; + } + + // ─── 统计 ───────────────────────────────────────────────────── + @Override + public Map stats(Long websiteId, Integer userId) { + LambdaQueryWrapper base = new LambdaQueryWrapper() + .eq(AppTicket::getDeleted, 0) + .eq(ObjectUtil.isNotNull(websiteId), AppTicket::getWebsiteId, websiteId) + .eq(ObjectUtil.isNotNull(userId), AppTicket::getSubmitUserId, userId); + + Map result = new HashMap<>(); + result.put("total", count(base.clone())); + result.put("pending", count(base.clone().in(AppTicket::getStatus, "pending", "assigned"))); + result.put("processing", count(base.clone().eq(AppTicket::getStatus, "processing"))); + result.put("resolved", count(base.clone().eq(AppTicket::getStatus, "resolved"))); + result.put("closed", count(base.clone().eq(AppTicket::getStatus, "closed"))); + return result; + } + + // ─── 获取技术人员列表 ───────────────────────────────────────── + @Override + public List> getTechStaffList() { + List members = appUserService.list( + new LambdaQueryWrapper() + .eq(AppUser::getStatus, 0) + .in(AppUser::getRole, "owner", "admin", "developer") + .select(AppUser::getUserId, AppUser::getNickname, AppUser::getAvatar)); + + // 去重(一个用户可能在多个应用里) + Map dedupMap = new LinkedHashMap<>(); + for (AppUser m : members) { + dedupMap.put(m.getUserId(), m); + } + + return dedupMap.values().stream().map(m -> { + Map map = new HashMap<>(); + map.put("userId", m.getUserId()); + map.put("nickname", m.getNickname() != null ? m.getNickname() : "用户" + m.getUserId()); + map.put("avatar", m.getAvatar()); + return map; + }).collect(Collectors.toList()); + } +} From 85e0b062b8506db5cd2e1dd5b7a28242aff227fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 30 Mar 2026 12:28:27 +0800 Subject: [PATCH 11/21] =?UTF-8?q?feat(ticket):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=B7=A5=E5=8D=95=E7=B3=BB=E7=BB=9F=E5=B9=B6=E9=9B=86=E6=88=90?= =?UTF-8?q?=E4=BC=81=E4=B8=9A=E5=BE=AE=E4=BF=A1=E9=A3=9E=E4=B9=A6=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在控制器中统一返回格式,修复fail方法调用 - 实现工单提交、查询、回复等核心功能 - 添加工单状态管理(待处理、已分配、处理中、已解决、已关闭) - 集成企业微信群机器人和飞书群机器人实时通知 - 实现异步推送机制支持四种通知场景:新工单、重新分配、新回复、状态变更 - 添加工单统计功能和用户权限控制 - 创建工单主表和回复表的数据库结构定义 --- docs/sql/2026-03-30_app_ticket_tables.sql | 48 +++ .../app/controller/AppTicketController.java | 16 +- .../service/impl/AppTicketServiceImpl.java | 310 +++++++++++++++++- 3 files changed, 349 insertions(+), 25 deletions(-) create mode 100644 docs/sql/2026-03-30_app_ticket_tables.sql diff --git a/docs/sql/2026-03-30_app_ticket_tables.sql b/docs/sql/2026-03-30_app_ticket_tables.sql new file mode 100644 index 0000000..6d621e4 --- /dev/null +++ b/docs/sql/2026-03-30_app_ticket_tables.sql @@ -0,0 +1,48 @@ +-- 工单主表 +CREATE TABLE IF NOT EXISTS `app_ticket` ( + `ticket_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '工单ID', + `ticket_no` VARCHAR(32) NOT NULL COMMENT '工单编号(TK-yyyyMMddHHmmss+4位随机)', + `title` VARCHAR(200) NOT NULL COMMENT '工单标题', + `content` TEXT NOT NULL COMMENT '工单内容描述', + `website_id` BIGINT DEFAULT NULL COMMENT '关联应用ID', + `website_name` VARCHAR(100) DEFAULT NULL COMMENT '应用名称(冗余)', + `category` VARCHAR(30) NOT NULL DEFAULT 'other' COMMENT '分类: bug/feature/consultation/complaint/other', + `priority` VARCHAR(20) NOT NULL DEFAULT 'normal' COMMENT '优先级: low/normal/high/urgent', + `status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/assigned/processing/resolved/closed/rejected', + `attachments` TEXT DEFAULT NULL COMMENT '附件JSON数组', + `submit_user_id` INT NOT NULL COMMENT '提交人用户ID', + `submit_user_name` VARCHAR(50) DEFAULT NULL COMMENT '提交人昵称(冗余)', + `submit_user_avatar` VARCHAR(500) DEFAULT NULL COMMENT '提交人头像(冗余)', + `assignee_id` INT DEFAULT NULL COMMENT '处理人用户ID', + `assignee_name` VARCHAR(50) DEFAULT NULL COMMENT '处理人昵称(冗余)', + `assignee_avatar` VARCHAR(500) DEFAULT NULL COMMENT '处理人头像(冗余)', + `reply_count` INT NOT NULL DEFAULT 0 COMMENT '回复数量', + `resolved_time` DATETIME DEFAULT NULL COMMENT '解决时间', + `closed_time` DATETIME DEFAULT NULL COMMENT '关闭时间', + `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`ticket_id`), + UNIQUE KEY `uk_ticket_no` (`ticket_no`), + KEY `idx_submit_user` (`submit_user_id`), + KEY `idx_assignee` (`assignee_id`), + KEY `idx_website_status` (`website_id`, `status`), + KEY `idx_status_create` (`status`, `create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用工单'; + +-- 工单回复表 +CREATE TABLE IF NOT EXISTS `app_ticket_reply` ( + `reply_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '回复ID', + `ticket_id` BIGINT NOT NULL COMMENT '关联工单ID', + `content` TEXT NOT NULL COMMENT '回复内容', + `attachments` TEXT DEFAULT NULL COMMENT '附件JSON数组', + `user_id` INT NOT NULL COMMENT '回复人用户ID', + `user_name` VARCHAR(50) DEFAULT NULL COMMENT '回复人昵称(冗余)', + `user_avatar` VARCHAR(500) DEFAULT NULL COMMENT '回复人头像(冗余)', + `is_staff` TINYINT NOT NULL DEFAULT 0 COMMENT '是否技术人员/客服: 0否 1是', + `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`reply_id`), + KEY `idx_ticket_id` (`ticket_id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工单回复'; diff --git a/src/main/java/com/gxwebsoft/app/controller/AppTicketController.java b/src/main/java/com/gxwebsoft/app/controller/AppTicketController.java index 68c718b..a247aee 100644 --- a/src/main/java/com/gxwebsoft/app/controller/AppTicketController.java +++ b/src/main/java/com/gxwebsoft/app/controller/AppTicketController.java @@ -38,7 +38,7 @@ public class AppTicketController extends BaseController { @GetMapping("/my") public ApiResult> myTickets(AppTicketParam param) { User loginUser = getLoginUser(); - if (loginUser == null) return fail("请先登录"); + if (loginUser == null) return fail("请先登录",null); return success(appTicketService.myPage(param, loginUser.getUserId())); } @@ -46,12 +46,12 @@ public class AppTicketController extends BaseController { @PostMapping("/submit") public ApiResult submit(@RequestBody AppTicket ticket) { User loginUser = getLoginUser(); - if (loginUser == null) return fail("请先登录"); + if (loginUser == null) return fail("请先登录",null); try { AppTicket result = appTicketService.submit(ticket, loginUser.getUserId()); return success("工单提交成功", result); } catch (Exception e) { - return fail(e.getMessage()); + return fail(e.getMessage(),null); } } @@ -74,7 +74,7 @@ public class AppTicketController extends BaseController { @GetMapping("/list") public ApiResult> allTickets(AppTicketParam param) { User loginUser = getLoginUser(); - if (loginUser == null) return fail("请先登录"); + if (loginUser == null) return fail("请先登录",null); return success(appTicketService.allPage(param)); } @@ -118,15 +118,15 @@ public class AppTicketController extends BaseController { @PostMapping("/reply") public ApiResult reply(@RequestBody AppTicketReply reply) { User loginUser = getLoginUser(); - if (loginUser == null) return fail("请先登录"); + if (loginUser == null) return fail("请先登录",null); if (reply.getContent() == null || reply.getContent().trim().isEmpty()) { - return fail("回复内容不能为空"); + return fail("回复内容不能为空",null); } try { AppTicketReply result = appTicketService.addReply(reply, loginUser.getUserId()); return success("回复成功", result); } catch (Exception e) { - return fail(e.getMessage()); + return fail(e.getMessage(),null); } } @@ -137,7 +137,7 @@ public class AppTicketController extends BaseController { public ApiResult> stats( @RequestParam(required = false) Long websiteId) { User loginUser = getLoginUser(); - if (loginUser == null) return fail("请先登录"); + if (loginUser == null) return fail("请先登录",null); // 技术端不限制用户维度;客户端通过路由区分 return success(appTicketService.stats(websiteId, null)); } diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java index dd98f05..f50fc51 100644 --- a/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java @@ -1,10 +1,14 @@ package com.gxwebsoft.app.service.impl; -import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.app.entity.AppTicket; import com.gxwebsoft.app.entity.AppTicketReply; import com.gxwebsoft.app.entity.AppUser; @@ -13,11 +17,12 @@ import com.gxwebsoft.app.mapper.AppTicketReplyMapper; import com.gxwebsoft.app.param.AppTicketParam; import com.gxwebsoft.app.service.AppTicketService; import com.gxwebsoft.app.service.AppUserService; -import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.service.UserService; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -37,6 +42,14 @@ import java.util.stream.Collectors; @Service public class AppTicketServiceImpl extends ServiceImpl implements AppTicketService { + /** 企业微信群机器人 Webhook,留空则不发送 */ + @Value("${notify.wecom-webhook:}") + private String wecomWebhook; + + /** 飞书群机器人 Webhook,留空则不发送 */ + @Value("${notify.feishu-webhook:}") + private String feishuWebhook; + @Resource private AppTicketReplyMapper replyMapper; @@ -56,10 +69,8 @@ public class AppTicketServiceImpl extends ServiceImpl myPage(AppTicketParam param, Integer userId) { - com.baomidou.mybatisplus.extension.plugins.pagination.Page page = - new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>( - param.getPageNum() != null ? param.getPageNum() : 1, - param.getPageSize() != null ? param.getPageSize() : 15); + // 用 PageParam 包装分页参数(getCurrent/getSize 来自父类 Page) + Page page = new Page<>(param.getCurrent(), param.getSize() > 0 ? param.getSize() : 15); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(AppTicket::getDeleted, 0) @@ -79,10 +90,7 @@ public class AppTicketServiceImpl extends ServiceImpl allPage(AppTicketParam param) { - com.baomidou.mybatisplus.extension.plugins.pagination.Page page = - new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>( - param.getPageNum() != null ? param.getPageNum() : 1, - param.getPageSize() != null ? param.getPageSize() : 20); + Page page = new Page<>(param.getCurrent(), param.getSize() > 0 ? param.getSize() : 20); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(AppTicket::getDeleted, 0) @@ -132,6 +140,10 @@ public class AppTicketServiceImpl extends ServiceImpl() .eq(AppTicket::getTicketId, reply.getTicketId()) .setSql("reply_count = reply_count + 1") .set(AppTicket::getUpdateTime, LocalDateTime.now()) - // 若状态是 assigned,更新为 processing .eq(AppTicket::getStatus, "assigned") .set(AppTicket::getStatus, "processing")); + // 异步推送:有新回复时通知对方 + if (ticket != null) sendReplyNotifyAsync(ticket, reply); + return reply; } @@ -269,11 +293,11 @@ public class AppTicketServiceImpl extends ServiceImpl result = new HashMap<>(); - result.put("total", count(base.clone())); - result.put("pending", count(base.clone().in(AppTicket::getStatus, "pending", "assigned"))); - result.put("processing", count(base.clone().eq(AppTicket::getStatus, "processing"))); - result.put("resolved", count(base.clone().eq(AppTicket::getStatus, "resolved"))); - result.put("closed", count(base.clone().eq(AppTicket::getStatus, "closed"))); + result.put("total", (long) count(base.clone())); + result.put("pending", (long) count(base.clone().in(AppTicket::getStatus, "pending", "assigned"))); + result.put("processing", (long) count(base.clone().eq(AppTicket::getStatus, "processing"))); + result.put("resolved", (long) count(base.clone().eq(AppTicket::getStatus, "resolved"))); + result.put("closed", (long) count(base.clone().eq(AppTicket::getStatus, "closed"))); return result; } @@ -300,4 +324,256 @@ public class AppTicketServiceImpl extends ServiceImpl **工单号:** %s\n" + + "> **标题:** %s\n" + + "> **分类:** %s **优先级:** %s\n" + + "> **提交人:** %s\n" + + "> %s\n" + + "> **描述:** %s", + title, + nullSafe(ticket.getTicketNo()), + nullSafe(ticket.getTitle()), + categoryLabel(ticket.getCategory()), priorityLabel(ticket.getPriority()), + nullSafe(ticket.getSubmitUserName()), + assigneeLine, + truncate(ticket.getContent(), 120) + ); + + List feishuLines = Arrays.asList( + new String[]{"工单号:" + nullSafe(ticket.getTicketNo())}, + new String[]{"标 题:" + nullSafe(ticket.getTitle())}, + new String[]{"分 类:" + categoryLabel(ticket.getCategory()) + " 优先级:" + priorityLabel(ticket.getPriority())}, + new String[]{"提交人:" + nullSafe(ticket.getSubmitUserName())}, + new String[]{assigneeLine.replace("**", "")}, + new String[]{"描 述:" + truncate(ticket.getContent(), 120)} + ); + + doSend(title, wecomContent, feishuLines, "ticket_created"); + } + + /** + * 场景2:工单被重新分配 → 通知新处理人 + */ + @Async + public void sendTicketAssignedAsync(AppTicket ticket) { + String now = now(); + String title = String.format("📋 工单已分配给您(%s)", now); + + String wecomContent = String.format( + "## %s\n" + + "> **工单号:** %s\n" + + "> **标题:** %s\n" + + "> **分配给:** %s\n" + + "> **分类:** %s **优先级:** %s\n" + + "> **描述:** %s", + title, + nullSafe(ticket.getTicketNo()), + nullSafe(ticket.getTitle()), + nullSafe(ticket.getAssigneeName()), + categoryLabel(ticket.getCategory()), priorityLabel(ticket.getPriority()), + truncate(ticket.getContent(), 100) + ); + + List feishuLines = Arrays.asList( + new String[]{"工单号:" + nullSafe(ticket.getTicketNo())}, + new String[]{"标 题:" + nullSafe(ticket.getTitle())}, + new String[]{"分配给:" + nullSafe(ticket.getAssigneeName())}, + new String[]{"分 类:" + categoryLabel(ticket.getCategory()) + " 优先级:" + priorityLabel(ticket.getPriority())}, + new String[]{"描 述:" + truncate(ticket.getContent(), 100)} + ); + + doSend(title, wecomContent, feishuLines, "ticket_assigned"); + } + + /** + * 场景3:有新回复 → 技术人员回复则通知客户,客户回复则通知技术人员 + */ + @Async + public void sendReplyNotifyAsync(AppTicket ticket, AppTicketReply reply) { + boolean isStaff = Integer.valueOf(1).equals(reply.getIsStaff()); + String now = now(); + String who = isStaff ? "技术人员" : "用户"; + String notifyRole = isStaff ? "您的工单有新回复" : "工单有用户新回复"; + String title = String.format("💬 %s(%s)", notifyRole, now); + + String wecomContent = String.format( + "## %s\n" + + "> **工单号:** %s\n" + + "> **标题:** %s\n" + + "> **回复人:** %s(%s)\n" + + "> **回复内容:** %s", + title, + nullSafe(ticket.getTicketNo()), + nullSafe(ticket.getTitle()), + nullSafe(reply.getUserName()), who, + truncate(reply.getContent(), 200) + ); + + List feishuLines = Arrays.asList( + new String[]{"工单号:" + nullSafe(ticket.getTicketNo())}, + new String[]{"标 题:" + nullSafe(ticket.getTitle())}, + new String[]{"回复人:" + nullSafe(reply.getUserName()) + "(" + who + ")"}, + new String[]{"回复内容:" + truncate(reply.getContent(), 200)} + ); + + doSend(title, wecomContent, feishuLines, "ticket_reply"); + } + + /** + * 场景4:状态变更(已解决/已关闭)→ 通知提交人 + */ + @Async + public void sendStatusChangedAsync(AppTicket ticket, String status) { + String now = now(); + String emoji = "resolved".equals(status) ? "✅" : "🔒"; + String statusLabel = "resolved".equals(status) ? "已解决" : "已关闭"; + String title = String.format("%s 工单%s(%s)", emoji, statusLabel, now); + + String wecomContent = String.format( + "## %s\n" + + "> **工单号:** %s\n" + + "> **标题:** %s\n" + + "> **提交人:** %s\n" + + "> **处理人:** %s\n" + + "> **状态:** %s", + title, + nullSafe(ticket.getTicketNo()), + nullSafe(ticket.getTitle()), + nullSafe(ticket.getSubmitUserName()), + nullSafe(ticket.getAssigneeName()), + "resolved".equals(status) ? "info" : "comment", + statusLabel + ); + + List feishuLines = Arrays.asList( + new String[]{"工单号:" + nullSafe(ticket.getTicketNo())}, + new String[]{"标 题:" + nullSafe(ticket.getTitle())}, + new String[]{"提交人:" + nullSafe(ticket.getSubmitUserName())}, + new String[]{"处理人:" + nullSafe(ticket.getAssigneeName())}, + new String[]{"状 态:" + statusLabel} + ); + + doSend(title, wecomContent, feishuLines, "ticket_status"); + } + + /** + * 统一发送入口:企业微信 + 飞书 + */ + private void doSend(String title, String wecomContent, List feishuLines, String scene) { + if (StrUtil.isNotBlank(wecomWebhook)) { + try { + sendWecom(wecomContent); + } catch (Exception e) { + log.warn("[工单通知][{}] 企业微信发送失败: {}", scene, e.getMessage()); + } + } + if (StrUtil.isNotBlank(feishuWebhook)) { + try { + sendFeishu(title, feishuLines); + } catch (Exception e) { + log.warn("[工单通知][{}] 飞书发送失败: {}", scene, e.getMessage()); + } + } + } + + /** + * 企业微信群机器人 - markdown 消息 + * 文档:https://developer.work.weixin.qq.com/document/path/91770 + */ + private void sendWecom(String content) { + Map textMap = new HashMap<>(); + textMap.put("content", content); + Map payload = new HashMap<>(); + payload.put("msgtype", "markdown"); + payload.put("markdown", textMap); + String resp = HttpUtil.post(wecomWebhook, JSON.toJSONString(payload)); + log.info("[工单通知] 企业微信推送结果: {}", resp); + } + + /** + * 飞书群机器人 - 富文本(post)消息 + * 文档:https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot + */ + private void sendFeishu(String title, List lines) { + List>> content = new ArrayList<>(); + for (String[] line : lines) { + Map node = new HashMap<>(); + node.put("tag", "text"); + node.put("text", line[0]); + content.add(Collections.singletonList(node)); + } + + Map zhCn = new HashMap<>(); + zhCn.put("title", title); + zhCn.put("content", content); + Map postContent = new HashMap<>(); + postContent.put("zh_cn", zhCn); + Map post = new HashMap<>(); + post.put("content", postContent); + Map payload = new HashMap<>(); + payload.put("msg_type", "post"); + payload.put("content", post); + + String resp = HttpUtil.post(feishuWebhook, JSON.toJSONString(payload)); + log.info("[工单通知] 飞书推送结果: {}", resp); + } + + // ════════════════════════════════════════════════════════════ + // 工具方法 + // ════════════════════════════════════════════════════════════ + + private String now() { + return LocalDateTime.now().format(DateTimeFormatter.ofPattern("MM-dd HH:mm")); + } + + private String nullSafe(String s) { + return StrUtil.isBlank(s) ? "—" : s; + } + + private String truncate(String s, int max) { + if (StrUtil.isBlank(s)) return "—"; + return s.length() > max ? s.substring(0, max) + "…" : s; + } + + private String categoryLabel(String category) { + if (category == null) return "其他"; + return switch (category) { + case "bug" -> "Bug反馈"; + case "feature" -> "功能需求"; + case "config" -> "配置问题"; + case "performance" -> "性能问题"; + case "security" -> "安全问题"; + default -> category; + }; + } + + private String priorityLabel(String priority) { + if (priority == null) return "普通"; + return switch (priority) { + case "urgent" -> "🔴 紧急"; + case "high" -> "🟠 高"; + case "normal" -> "🟡 普通"; + case "low" -> "🟢 低"; + default -> priority; + }; + } } From ee2d95da91376b5bcf88816008928d45ca10dad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 30 Mar 2026 12:43:12 +0800 Subject: [PATCH 12/21] =?UTF-8?q?feat(website):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=BD=91=E7=AB=99=E6=9F=A5=E8=AF=A2=E4=B8=AD=E7=9A=84=E5=8D=8F?= =?UTF-8?q?=E4=BD=9C=E6=88=90=E5=91=98=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在CmsWebsiteParam中新增memberUserId参数用于协作成员筛选 - 修改CmsWebsiteMapper.xml添加按memberUserId查询的SQL逻辑 - 实现通过app_user表关联查询指定用户的协作网站列表 - 支持同时查询用户直接创建和作为成员参与的网站 - 保持原有websiteIds和keywords查询条件的兼容性 --- .../com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml | 9 +++++++++ .../java/com/gxwebsoft/cms/param/CmsWebsiteParam.java | 3 +++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml b/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml index 577c1c6..8d4fd7f 100644 --- a/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml +++ b/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml @@ -430,6 +430,15 @@ #{item} + + AND ( + a.user_id = #{param.memberUserId} + OR a.website_id IN ( + SELECT website_id FROM app_user + WHERE user_id = #{param.memberUserId} AND deleted = 0 + ) + ) + AND (a.website_name LIKE CONCAT('%', #{param.keywords}, '%') OR a.website_code LIKE CONCAT('%', #{param.keywords}, '%') diff --git a/src/main/java/com/gxwebsoft/cms/param/CmsWebsiteParam.java b/src/main/java/com/gxwebsoft/cms/param/CmsWebsiteParam.java index cafb74d..6236415 100644 --- a/src/main/java/com/gxwebsoft/cms/param/CmsWebsiteParam.java +++ b/src/main/java/com/gxwebsoft/cms/param/CmsWebsiteParam.java @@ -214,6 +214,9 @@ public class CmsWebsiteParam extends BaseParam { @Schema(description = "按WebsiteIds集搜索") private Set websiteIds; + @Schema(description = "协作成员userId(查该用户作为成员参与的应用,联表 app_user)") + private Integer memberUserId; + @Schema(description = "当前登录用户ID") @QueryField(type = QueryType.EQ) private Integer loginUserId; From 875111d2d84c4eb31f11e2521f9d2815a2ba091f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 30 Mar 2026 13:07:26 +0800 Subject: [PATCH 13/21] =?UTF-8?q?fix(mapper):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=BD=91=E7=AB=99=E6=9F=A5=E8=AF=A2=E4=B8=AD=E7=9A=84=E8=A1=A8?= =?UTF-8?q?=E5=88=AB=E5=90=8D=E5=BC=95=E7=94=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为app_user表添加别名au以避免字段冲突 - 确保website_id字段正确从别名表中引用 - 保持deleted字段的过滤条件一致性 --- .../java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml b/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml index 8d4fd7f..865bb37 100644 --- a/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml +++ b/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml @@ -434,8 +434,8 @@ AND ( a.user_id = #{param.memberUserId} OR a.website_id IN ( - SELECT website_id FROM app_user - WHERE user_id = #{param.memberUserId} AND deleted = 0 + SELECT au.website_id FROM app_user au + WHERE au.user_id = #{param.memberUserId} AND au.deleted = 0 ) ) From 35fdc2dcfc50a489b67e510c1bd0abacc48b71b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 30 Mar 2026 14:18:03 +0800 Subject: [PATCH 14/21] =?UTF-8?q?feat(entity):=20=E4=B8=BA=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E5=AE=9E=E4=BD=93=E6=B7=BB=E5=8A=A0=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E7=9A=84JSON=E5=BA=8F=E5=88=97=E5=8C=96?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在AppTicket和AppTicketReply实体中添加JsonArrayToStringDeserializer和JsonStringToArraySerializer注解 - 实现附件字段在数据库存储时从数组转换为JSON字符串 - 实现附件字段向前端响应时从JSON字符串转换为数组格式 - 添加JsonArrayToStringDeserializer类处理数组到字符串的反序列化 - 添加JsonStringToArraySerializer类处理字符串到数组的序列化 - 移除CMS网站映射器中的软删除条件过滤 --- .../com/gxwebsoft/app/entity/AppTicket.java | 6 ++ .../gxwebsoft/app/entity/AppTicketReply.java | 6 ++ .../cms/mapper/xml/CmsWebsiteMapper.xml | 2 +- .../config/JsonArrayToStringDeserializer.java | 57 +++++++++++++++++++ .../config/JsonStringToArraySerializer.java | 45 +++++++++++++++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gxwebsoft/common/core/config/JsonArrayToStringDeserializer.java create mode 100644 src/main/java/com/gxwebsoft/common/core/config/JsonStringToArraySerializer.java diff --git a/src/main/java/com/gxwebsoft/app/entity/AppTicket.java b/src/main/java/com/gxwebsoft/app/entity/AppTicket.java index 1d62783..a4aa64d 100644 --- a/src/main/java/com/gxwebsoft/app/entity/AppTicket.java +++ b/src/main/java/com/gxwebsoft/app/entity/AppTicket.java @@ -4,6 +4,10 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.gxwebsoft.common.core.config.JsonArrayToStringDeserializer; +import com.gxwebsoft.common.core.config.JsonStringToArraySerializer; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -53,6 +57,8 @@ public class AppTicket implements Serializable { private String status; @Schema(description = "附件JSON数组") + @JsonDeserialize(using = JsonArrayToStringDeserializer.class) + @JsonSerialize(using = JsonStringToArraySerializer.class) private String attachments; @Schema(description = "提交人用户ID") diff --git a/src/main/java/com/gxwebsoft/app/entity/AppTicketReply.java b/src/main/java/com/gxwebsoft/app/entity/AppTicketReply.java index fb13ca9..2633d94 100644 --- a/src/main/java/com/gxwebsoft/app/entity/AppTicketReply.java +++ b/src/main/java/com/gxwebsoft/app/entity/AppTicketReply.java @@ -4,6 +4,10 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.gxwebsoft.common.core.config.JsonArrayToStringDeserializer; +import com.gxwebsoft.common.core.config.JsonStringToArraySerializer; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -35,6 +39,8 @@ public class AppTicketReply implements Serializable { private String content; @Schema(description = "附件JSON数组") + @JsonDeserialize(using = JsonArrayToStringDeserializer.class) + @JsonSerialize(using = JsonStringToArraySerializer.class) private String attachments; @Schema(description = "回复人用户ID") diff --git a/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml b/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml index 865bb37..fb17bb1 100644 --- a/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml +++ b/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsWebsiteMapper.xml @@ -435,7 +435,7 @@ a.user_id = #{param.memberUserId} OR a.website_id IN ( SELECT au.website_id FROM app_user au - WHERE au.user_id = #{param.memberUserId} AND au.deleted = 0 + WHERE au.user_id = #{param.memberUserId} ) ) diff --git a/src/main/java/com/gxwebsoft/common/core/config/JsonArrayToStringDeserializer.java b/src/main/java/com/gxwebsoft/common/core/config/JsonArrayToStringDeserializer.java new file mode 100644 index 0000000..dbb4989 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/core/config/JsonArrayToStringDeserializer.java @@ -0,0 +1,57 @@ +package com.gxwebsoft.common.core.config; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.List; + +/** + * JSON 数组 ↔ String 反序列化器 + *

+ * 数据库字段存储的是 JSON 字符串(如 ["url1","url2"]), + * 前端传来的是 JSON 数组;此反序列化器把两种形式都正确处理为 String 存库。 + * + * @author WebSoft + * @since 2026-03-30 + */ +@Slf4j +public class JsonArrayToStringDeserializer extends JsonDeserializer { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // 如果前端传的已经是字符串,直接返回 + if (p.currentToken() == JsonToken.VALUE_STRING) { + String text = p.getText(); + if (text == null || text.trim().isEmpty()) { + return null; + } + return text; + } + // 如果前端传的是数组,序列化成 JSON 字符串 + if (p.currentToken() == JsonToken.START_ARRAY) { + List list = MAPPER.readValue(p, MAPPER.getTypeFactory() + .constructCollectionType(List.class, String.class)); + if (list == null || list.isEmpty()) { + return null; + } + return MAPPER.writeValueAsString(list); + } + // null + if (p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + return p.getValueAsString(); + } + + @Override + public String getNullValue(DeserializationContext ctxt) { + return null; + } +} diff --git a/src/main/java/com/gxwebsoft/common/core/config/JsonStringToArraySerializer.java b/src/main/java/com/gxwebsoft/common/core/config/JsonStringToArraySerializer.java new file mode 100644 index 0000000..b42390d --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/core/config/JsonStringToArraySerializer.java @@ -0,0 +1,45 @@ +package com.gxwebsoft.common.core.config; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** + * JSON 字符串 → 数组 序列化器 + *

+ * 数据库中 attachments 存储为 JSON 字符串,响应给前端时自动转成数组。 + * + * @author WebSoft + * @since 2026-03-30 + */ +@Slf4j +public class JsonStringToArraySerializer extends JsonSerializer { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null || value.trim().isEmpty()) { + gen.writeStartArray(); + gen.writeEndArray(); + return; + } + try { + List list = MAPPER.readValue(value, + MAPPER.getTypeFactory().constructCollectionType(List.class, String.class)); + gen.writeObject(list); + } catch (Exception e) { + // 解析失败(非JSON格式)就把原始字符串包成单元素数组 + log.warn("attachments 字段不是合法 JSON 数组,原样包装: {}", value); + gen.writeStartArray(); + gen.writeString(value); + gen.writeEndArray(); + } + } +} From 44e95a72730a5a8022b6edc076364f6b2c65d842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 30 Mar 2026 14:31:52 +0800 Subject: [PATCH 15/21] =?UTF-8?q?feat(ticket):=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=BF=BD=E7=95=A5=E7=A7=9F=E6=88=B7=E6=8B=A6=E6=88=AA=E5=99=A8?= =?UTF-8?q?=E7=9A=84=E6=96=B9=E6=B3=95=E8=8E=B7=E5=8F=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在工单创建时使用 userService.getByIdIgnoreTenant 替代 getById - 在工单分配时使用 userService.getByIdIgnoreTenant 替代 getById - 在回复创建时使用 userService.getByIdIgnoreTenant 替代 getById - 添加注释说明使用 @InterceptorIgnore 绕过租户拦截器跨库查询 - 确保用户信息获取不受租户隔离限制影响 --- .../app/service/impl/AppTicketServiceImpl.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java index f50fc51..0e0e913 100644 --- a/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java @@ -125,9 +125,9 @@ public class AppTicketServiceImpl extends ServiceImpl wrapper = new LambdaUpdateWrapper() .eq(AppTicket::getTicketId, ticketId) .set(AppTicket::getAssigneeId, assigneeId) @@ -244,9 +244,9 @@ public class AppTicketServiceImpl extends ServiceImpl Date: Mon, 30 Mar 2026 19:42:53 +0800 Subject: [PATCH 16/21] =?UTF-8?q?feat(app):=20=E6=B7=BB=E5=8A=A0=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建应用配置表及实体类 - 实现应用配置的增删改查接口 - 添加配置值加密解密功能 - 支持按应用ID批量获取配置映射 - 实现配置的批量保存和删除功能 - 添加分页查询和列表查询支持 - 集成租户隔离和软删除功能 --- docs/app_config.sql | 19 ++ .../app/controller/AppConfigController.java | 129 +++++++++ .../com/gxwebsoft/app/entity/AppConfig.java | 87 ++++++ .../gxwebsoft/app/mapper/AppConfigMapper.java | 43 +++ .../app/mapper/xml/AppConfigMapper.xml | 66 +++++ .../gxwebsoft/app/param/AppConfigParam.java | 38 +++ .../app/service/AppConfigService.java | 58 ++++ .../service/impl/AppConfigServiceImpl.java | 270 ++++++++++++++++++ 8 files changed, 710 insertions(+) create mode 100644 docs/app_config.sql create mode 100644 src/main/java/com/gxwebsoft/app/controller/AppConfigController.java create mode 100644 src/main/java/com/gxwebsoft/app/entity/AppConfig.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/AppConfigMapper.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/xml/AppConfigMapper.xml create mode 100644 src/main/java/com/gxwebsoft/app/param/AppConfigParam.java create mode 100644 src/main/java/com/gxwebsoft/app/service/AppConfigService.java create mode 100644 src/main/java/com/gxwebsoft/app/service/impl/AppConfigServiceImpl.java diff --git a/docs/app_config.sql b/docs/app_config.sql new file mode 100644 index 0000000..dfe7f81 --- /dev/null +++ b/docs/app_config.sql @@ -0,0 +1,19 @@ +-- 应用配置表 +CREATE TABLE app_config ( + config_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '配置ID', + website_id INT NOT NULL COMMENT '应用ID', + config_key VARCHAR(100) NOT NULL COMMENT '配置键', + config_value TEXT NOT NULL COMMENT '配置值(JSON或字符串)', + config_type VARCHAR(50) NOT NULL DEFAULT 'general' COMMENT '配置类型:general/api/callback/wechat/payment/git等', + is_encrypted TINYINT DEFAULT 0 COMMENT '是否加密 0否 1是', + is_secret TINYINT DEFAULT 0 COMMENT '是否敏感信息 0否 1是', + description VARCHAR(500) DEFAULT '' COMMENT '配置说明', + sort_number INT DEFAULT 0 COMMENT '排序号', + tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户id', + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + deleted TINYINT DEFAULT 0 COMMENT '是否删除 0否 1是', + UNIQUE KEY uk_website_key (website_id, config_key, deleted), + INDEX idx_website_type (website_id, config_type), + INDEX idx_tenant (tenant_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用配置表'; diff --git a/src/main/java/com/gxwebsoft/app/controller/AppConfigController.java b/src/main/java/com/gxwebsoft/app/controller/AppConfigController.java new file mode 100644 index 0000000..39fab56 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/controller/AppConfigController.java @@ -0,0 +1,129 @@ +package com.gxwebsoft.app.controller; + +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.app.entity.AppConfig; +import com.gxwebsoft.app.param.AppConfigParam; +import com.gxwebsoft.app.service.AppConfigService; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.PageResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * 应用配置表 Controller + */ +@Slf4j +@Tag(name = "应用配置管理") +@RestController +@RequestMapping("/api/app/app-config") +public class AppConfigController extends BaseController { + + @Resource + private AppConfigService appConfigService; + + /** + * 分页查询应用配置 + */ + @Operation(summary = "分页查询应用配置") + @GetMapping("/page") + public ApiResult> page(AppConfigParam param) { + return success(new PageResult<>(appConfigService.page(param))); + } + + /** + * 获取应用配置列表 + */ + @Operation(summary = "获取应用配置列表") + @GetMapping() + public ApiResult> list(AppConfigParam param) { + return success(appConfigService.list(param)); + } + + /** + * 根据应用ID获取配置映射 + */ + @Operation(summary = "根据应用ID获取配置映射") + @GetMapping("/map/{websiteId}") + public ApiResult> getConfigsByWebsiteId(@PathVariable Integer websiteId) { + return success(appConfigService.getConfigsByWebsiteId(websiteId)); + } + + /** + * 获取单个配置值 + */ + @Operation(summary = "获取单个配置值") + @GetMapping("/value") + public ApiResult getConfigValue(@RequestParam Integer websiteId, @RequestParam String configKey) { + return success(appConfigService.getConfigValue(websiteId, configKey),null); + } + + /** + * 保存配置 + */ + @Operation(summary = "保存配置") + @PostMapping() + public ApiResult save(@RequestBody AppConfig config) { + appConfigService.saveConfig(config); + return success("保存成功"); + } + + /** + * 批量保存配置 + */ + @Operation(summary = "批量保存配置") + @PostMapping("/batch") + public ApiResult batchSave(@RequestBody BatchSaveRequest request) { + appConfigService.batchSaveConfig(request.getWebsiteId(), request.getConfigs()); + return success("保存成功"); + } + + /** + * 更新配置 + */ + @Operation(summary = "更新配置") + @PutMapping() + public ApiResult update(@RequestBody AppConfig config) { + appConfigService.updateConfig(config); + return success("更新成功"); + } + + /** + * 删除配置 + */ + @Operation(summary = "删除配置") + @DeleteMapping("/{configId}") + public ApiResult delete(@PathVariable Integer configId) { + appConfigService.deleteConfig(configId); + return success("删除成功"); + } + + /** + * 批量保存请求对象 + */ + public static class BatchSaveRequest { + private Integer websiteId; + private List configs; + + public Integer getWebsiteId() { + return websiteId; + } + + public void setWebsiteId(Integer websiteId) { + this.websiteId = websiteId; + } + + public List getConfigs() { + return configs; + } + + public void setConfigs(List configs) { + this.configs = configs; + } + } +} diff --git a/src/main/java/com/gxwebsoft/app/entity/AppConfig.java b/src/main/java/com/gxwebsoft/app/entity/AppConfig.java new file mode 100644 index 0000000..d8c77df --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/entity/AppConfig.java @@ -0,0 +1,87 @@ +package com.gxwebsoft.app.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * 应用配置表 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("app_config") +public class AppConfig implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 配置ID + */ + @TableId(value = "config_id", type = IdType.AUTO) + private Integer configId; + + /** + * 应用ID + */ + private Integer websiteId; + + /** + * 配置键 + */ + private String configKey; + + /** + * 配置值(JSON或字符串) + */ + private String configValue; + + /** + * 配置类型:general/api/callback/wechat/payment/git等 + */ + private String configType; + + /** + * 是否加密 0否 1是 + */ + private Integer isEncrypted; + + /** + * 是否敏感信息 0否 1是 + */ + private Integer isSecret; + + /** + * 配置说明 + */ + private String description; + + /** + * 排序号 + */ + private Integer sortNumber; + + /** + * 租户id + */ + private Long tenantId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private Long createdTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long updatedTime; + + /** + * 是否删除 0否 1是 + */ + @TableLogic + private Integer deleted; +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/AppConfigMapper.java b/src/main/java/com/gxwebsoft/app/mapper/AppConfigMapper.java new file mode 100644 index 0000000..1b428b4 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/AppConfigMapper.java @@ -0,0 +1,43 @@ +package com.gxwebsoft.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.app.entity.AppConfig; +import com.gxwebsoft.app.param.AppConfigParam; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 应用配置表 Mapper + * + * @author 科技小王子 + */ +@Mapper +public interface AppConfigMapper extends BaseMapper { + + /** + * 批量获取应用配置 + */ + List> selectConfigsByWebsiteId(Integer websiteId); + + /** + * 分页查询 + * + * @param page 分页对象 + * @param param 查询参数 + * @return List + */ + List selectPageRel(@Param("page") IPage page, + @Param("param") AppConfigParam param); + + /** + * 查询全部 + * + * @param param 查询参数 + * @return List + */ + List selectListRel(@Param("param") AppConfigParam param); +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppConfigMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppConfigMapper.xml new file mode 100644 index 0000000..c4679fa --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppConfigMapper.xml @@ -0,0 +1,66 @@ + + + + + + + SELECT a.* + FROM app_config a + + a.deleted = 0 + + AND a.config_id = #{param.configId} + + + AND a.website_id = #{param.websiteId} + + + AND a.config_key LIKE CONCAT('%', #{param.configKey}, '%') + + + AND a.config_type = #{param.configType} + + + AND a.is_secret = #{param.isSecret} + + + AND a.tenant_id = #{param.tenantId} + + + AND a.create_time >= #{param.createTimeStart} + + + AND a.create_time <= #{param.createTimeEnd} + + + AND a.config_key LIKE CONCAT('%', #{param.keywords}, '%') + + + + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/app/param/AppConfigParam.java b/src/main/java/com/gxwebsoft/app/param/AppConfigParam.java new file mode 100644 index 0000000..48dcfc7 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/param/AppConfigParam.java @@ -0,0 +1,38 @@ +package com.gxwebsoft.app.param; + +import com.gxwebsoft.common.core.web.BaseParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应用配置表查询参数 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class AppConfigParam extends BaseParam { + + /** + * 配置ID + */ + private Integer configId; + + /** + * 应用ID + */ + private Integer websiteId; + + /** + * 配置键 + */ + private String configKey; + + /** + * 配置类型 + */ + private String configType; + + /** + * 是否敏感信息 + */ + private Integer isSecret; +} diff --git a/src/main/java/com/gxwebsoft/app/service/AppConfigService.java b/src/main/java/com/gxwebsoft/app/service/AppConfigService.java new file mode 100644 index 0000000..287d314 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/AppConfigService.java @@ -0,0 +1,58 @@ +package com.gxwebsoft.app.service; + +import com.gxwebsoft.app.entity.AppConfig; +import com.gxwebsoft.app.param.AppConfigParam; + +import java.util.List; +import java.util.Map; + +/** + * 应用配置表 Service + */ +public interface AppConfigService { + + /** + * 分页查询应用配置 + */ + List page(AppConfigParam param); + + /** + * 获取应用配置列表 + */ + List list(AppConfigParam param); + + /** + * 根据应用ID获取配置映射(自动解密) + */ + Map getConfigsByWebsiteId(Integer websiteId); + + /** + * 获取单个配置值 + */ + String getConfigValue(Integer websiteId, String configKey); + + /** + * 保存配置 + */ + void saveConfig(AppConfig config); + + /** + * 批量保存配置 + */ + void batchSaveConfig(Integer websiteId, List configs); + + /** + * 更新配置 + */ + void updateConfig(AppConfig config); + + /** + * 删除配置 + */ + void deleteConfig(Integer configId); + + /** + * 根据应用ID删除所有配置 + */ + void deleteByWebsiteId(Integer websiteId); +} diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppConfigServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppConfigServiceImpl.java new file mode 100644 index 0000000..fa8464e --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppConfigServiceImpl.java @@ -0,0 +1,270 @@ +package com.gxwebsoft.app.service.impl; + +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.symmetric.AES; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.app.entity.AppConfig; +import com.gxwebsoft.app.mapper.AppConfigMapper; +import com.gxwebsoft.app.param.AppConfigParam; +import com.gxwebsoft.app.service.AppConfigService; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.common.system.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 应用配置表 Service 实现类 + * + * @author 科技小王子 + */ +@Slf4j +@Service +public class AppConfigServiceImpl extends ServiceImpl implements AppConfigService { + + @Resource + private UserService userService; + + /** + * 配置加密密钥(从配置文件读取) + */ + @Value("${app.config.encrypt-key:GXWebsoft2024!@#$}") + private String encryptKey; + + /** + * 分页查询应用配置 + */ + @Override + public List page(AppConfigParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("sort_number asc, config_id asc"); + List list = baseMapper.selectPageRel(page, param); + return page.sortRecords(list); + } + + /** + * 分页查询应用配置(返回PageResult) + */ + public PageResult pageRel(AppConfigParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("sort_number asc, config_id asc"); + List list = baseMapper.selectPageRel(page, param); + return new PageResult<>(list, page.getTotal()); + } + + /** + * 获取应用配置列表 + */ + @Override + public List list(AppConfigParam param) { + List list = baseMapper.selectListRel(param); + PageParam page = new PageParam<>(); + page.setDefaultOrder("sort_number asc, config_id asc"); + return page.sortRecords(list); + } + + /** + * 根据应用ID获取配置映射(自动解密) + */ + @Override + public Map getConfigsByWebsiteId(Integer websiteId) { + List> configs = baseMapper.selectConfigsByWebsiteId(websiteId); + Map result = new HashMap<>(); + + for (Map config : configs) { + String configKey = (String) config.get("configKey"); + Object configValue = config.get("configValue"); + Integer isEncrypted = (Integer) config.get("isEncrypted"); + + // 解密 + if (isEncrypted != null && isEncrypted == 1 && configValue != null) { + try { + configValue = decrypt((String) configValue); + } catch (Exception e) { + log.error("配置解密失败: {}", configKey, e); + } + } + + result.put(configKey, configValue); + } + + return result; + } + + /** + * 获取单个配置值 + */ + @Override + public String getConfigValue(Integer websiteId, String configKey) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(AppConfig::getWebsiteId, websiteId); + wrapper.eq(AppConfig::getConfigKey, configKey); + AppConfig config = getOne(wrapper); + + if (config == null || config.getConfigValue() == null) { + return null; + } + + // 解密 + if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1) { + return decrypt(config.getConfigValue()); + } + + return config.getConfigValue(); + } + + /** + * 保存配置 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void saveConfig(AppConfig config) { + // 设置租户ID + Integer tenantId = getCurrentTenantId(); + if (tenantId != null) { + config.setTenantId(Long.valueOf(tenantId)); + } + + // 加密敏感信息 + if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1 && config.getConfigValue() != null) { + config.setConfigValue(encrypt(config.getConfigValue())); + } + + save(config); + } + + /** + * 批量保存配置 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void batchSaveConfig(Integer websiteId, List configs) { + // 设置租户ID + Integer tenantId = getCurrentTenantId(); + if (tenantId != null) { + for (AppConfig config : configs) { + config.setTenantId(Long.valueOf(tenantId)); + } + } + + // 先删除该应用的所有配置 + remove(new LambdaQueryWrapper() + .eq(AppConfig::getWebsiteId, websiteId)); + + // 批量插入新配置 + for (AppConfig config : configs) { + config.setWebsiteId(websiteId); + + // 加密敏感信息 + if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1 && config.getConfigValue() != null) { + config.setConfigValue(encrypt(config.getConfigValue())); + } + + save(config); + } + } + + /** + * 更新配置 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateConfig(AppConfig config) { + // 加密敏感信息 + if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1 && config.getConfigValue() != null) { + config.setConfigValue(encrypt(config.getConfigValue())); + } + + updateById(config); + } + + /** + * 删除配置 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteConfig(Integer configId) { + removeById(configId); + } + + /** + * 根据应用ID删除所有配置 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByWebsiteId(Integer websiteId) { + remove(new LambdaQueryWrapper() + .eq(AppConfig::getWebsiteId, websiteId)); + } + + /** + * 构建查询条件 + */ + private LambdaQueryWrapper buildQueryWrapper(AppConfigParam param) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (param.getConfigId() != null) { + wrapper.eq(AppConfig::getConfigId, param.getConfigId()); + } + if (param.getWebsiteId() != null) { + wrapper.eq(AppConfig::getWebsiteId, param.getWebsiteId()); + } + if (param.getConfigKey() != null) { + wrapper.like(AppConfig::getConfigKey, param.getConfigKey()); + } + if (param.getConfigType() != null) { + wrapper.eq(AppConfig::getConfigType, param.getConfigType()); + } + if (param.getIsSecret() != null) { + wrapper.eq(AppConfig::getIsSecret, param.getIsSecret()); + } + + wrapper.orderByAsc(AppConfig::getConfigType) + .orderByAsc(AppConfig::getSortNumber) + .orderByAsc(AppConfig::getConfigId); + + return wrapper; + } + + /** + * AES 加密 + */ + private String encrypt(String plainText) { + AES aes = SecureUtil.aes(encryptKey.getBytes()); + return aes.encryptBase64(plainText); + } + + /** + * AES 解密 + */ + private String decrypt(String cipherText) { + AES aes = SecureUtil.aes(encryptKey.getBytes()); + return aes.decryptStr(cipherText); + } + + /** + * 获取当前登录用户的租户ID + */ + private Integer getCurrentTenantId() { + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getPrincipal() instanceof User) { + return ((User) authentication.getPrincipal()).getTenantId(); + } + } catch (Exception e) { + log.error("获取当前用户租户ID失败", e); + } + return null; + } +} From 38ee4c65e6c392b36c01495ff1ee1505f763c4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Tue, 31 Mar 2026 19:59:24 +0800 Subject: [PATCH 17/21] =?UTF-8?q?feat(app):=20=E6=B7=BB=E5=8A=A0=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E8=80=85=E8=B5=84=E6=BA=90=E7=AE=A1=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建app_resource数据表,支持服务器/数据库/云存储/域名/SSL证书等资源管理 - 实现AppResource实体类,包含各类资源的特定字段和通用字段 - 开发AppResourceController控制器,提供资源的增删改查和统计功能 - 实现AppResourceService服务层,包含业务逻辑和数据操作方法 - 创建AppResourceMapper数据访问层,支持分页查询和关联查询 - 添加AppResourceParam查询参数类,支持多条件筛选 - 集成MyBatis XML映射文件,实现复杂的关联查询和统计功能 - 实现基于用户权限的资源访问控制和逻辑删除机制 --- .../app/controller/AppResourceController.java | 145 ++++++++++++++++++ .../com/gxwebsoft/app/entity/AppResource.java | 121 +++++++++++++++ .../app/mapper/AppResourceMapper.java | 35 +++++ .../app/mapper/xml/AppResourceMapper.xml | 75 +++++++++ .../gxwebsoft/app/param/AppResourceParam.java | 54 +++++++ .../app/service/AppResourceService.java | 39 +++++ .../service/impl/AppResourceServiceImpl.java | 109 +++++++++++++ .../com/gxwebsoft/app/sql/app_resource.sql | 54 +++++++ 8 files changed, 632 insertions(+) create mode 100644 src/main/java/com/gxwebsoft/app/controller/AppResourceController.java create mode 100644 src/main/java/com/gxwebsoft/app/entity/AppResource.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/AppResourceMapper.java create mode 100644 src/main/java/com/gxwebsoft/app/mapper/xml/AppResourceMapper.xml create mode 100644 src/main/java/com/gxwebsoft/app/param/AppResourceParam.java create mode 100644 src/main/java/com/gxwebsoft/app/service/AppResourceService.java create mode 100644 src/main/java/com/gxwebsoft/app/service/impl/AppResourceServiceImpl.java create mode 100644 src/main/java/com/gxwebsoft/app/sql/app_resource.sql diff --git a/src/main/java/com/gxwebsoft/app/controller/AppResourceController.java b/src/main/java/com/gxwebsoft/app/controller/AppResourceController.java new file mode 100644 index 0000000..35a8273 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/controller/AppResourceController.java @@ -0,0 +1,145 @@ +package com.gxwebsoft.app.controller; + +import com.gxwebsoft.app.entity.AppResource; +import com.gxwebsoft.app.param.AppResourceParam; +import com.gxwebsoft.app.service.AppResourceService; +import com.gxwebsoft.common.core.annotation.OperationLog; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.system.entity.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * 开发者资源管理控制器(服务器/数据库/云存储/域名/SSL) + * + * @author 科技小王子 + * @since 2026-03-31 + */ +@Slf4j +@Tag(name = "开发者资源管理") +@RestController +@RequestMapping("/api/_modules/app/developer-resource") +public class AppResourceController extends BaseController { + + @Resource + private AppResourceService appResourceService; + + // ─── 查询接口 ───────────────────────────────────────────────── + + @Operation(summary = "分页查询资源列表") + @GetMapping("/page") + public ApiResult> page(AppResourceParam param) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + // 普通开发者只能查自己的资源 + param.setUserId(loginUser.getUserId()); + param.setTenantId(loginUser.getTenantId()); + return success(appResourceService.pageRel(param)); + } + + @Operation(summary = "查询资源列表(不分页)") + @GetMapping + public ApiResult> list(AppResourceParam param) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + param.setUserId(loginUser.getUserId()); + param.setTenantId(loginUser.getTenantId()); + return success(appResourceService.listRel(param)); + } + + @Operation(summary = "获取资源详情") + @GetMapping("/{resourceId}") + public ApiResult get(@PathVariable Long resourceId) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + AppResource resource = appResourceService.getByIdRel(resourceId); + if (resource == null) return fail("资源不存在", null); + if (!resource.getUserId().equals(loginUser.getUserId())) return fail("无权访问此资源", null); + return success(resource); + } + + @Operation(summary = "统计各类型资源数量") + @GetMapping("/stats") + public ApiResult> stats() { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + return success(appResourceService.countByType(loginUser.getUserId(), loginUser.getTenantId())); + } + + // ─── 新增/修改接口 ──────────────────────────────────────────── + + @OperationLog + @Operation(summary = "新增资源") + @PostMapping + public ApiResult save(@RequestBody AppResource resource) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + if (resource.getResourceType() == null || resource.getResourceType().isEmpty()) { + return fail("资源类型不能为空", null); + } + if (resource.getName() == null || resource.getName().isEmpty()) { + return fail("资源名称不能为空", null); + } + resource.setTenantId(loginUser.getTenantId()); + try { + AppResource result = appResourceService.addResource(resource, loginUser.getUserId()); + return success("添加成功", result); + } catch (Exception e) { + return fail(e.getMessage(), null); + } + } + + @OperationLog + @Operation(summary = "修改资源") + @PutMapping + public ApiResult update(@RequestBody AppResource resource) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + try { + AppResource result = appResourceService.updateResource(resource); + return success("修改成功", result); + } catch (Exception e) { + return fail(e.getMessage(), null); + } + } + + // ─── 删除接口 ───────────────────────────────────────────────── + + @OperationLog + @Operation(summary = "删除资源(逻辑删除)") + @DeleteMapping("/{resourceId}") + public ApiResult remove(@PathVariable Long resourceId) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + try { + appResourceService.removeResource(resourceId, loginUser.getUserId()); + return success("删除成功"); + } catch (Exception e) { + return fail(e.getMessage()); + } + } + + @OperationLog + @Operation(summary = "批量删除资源") + @DeleteMapping("/batch") + public ApiResult removeBatch(@RequestBody List ids) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + try { + for (Long id : ids) { + appResourceService.removeResource(id, loginUser.getUserId()); + } + return success("批量删除成功"); + } catch (Exception e) { + return fail(e.getMessage()); + } + } +} diff --git a/src/main/java/com/gxwebsoft/app/entity/AppResource.java b/src/main/java/com/gxwebsoft/app/entity/AppResource.java new file mode 100644 index 0000000..2027876 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/entity/AppResource.java @@ -0,0 +1,121 @@ +package com.gxwebsoft.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 开发者资源(服务器/数据库/云存储/域名/SSL证书) + * + * @author 科技小王子 + * @since 2026-03-31 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("app_resource") +@Schema(name = "AppResource对象", description = "开发者资源") +public class AppResource implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "资源ID") + @TableId(value = "resource_id", type = IdType.AUTO) + private Long resourceId; + + @Schema(description = "资源类型: server/database/storage/domain/ssl") + private String resourceType; + + @Schema(description = "资源名称") + private String name; + + @Schema(description = "服务商: tencent/aliyun/huawei/other") + private String provider; + + @Schema(description = "关联应用ID(可选)") + private Long websiteId; + + @Schema(description = "关联应用名称(冗余)") + private String websiteName; + + // ─── 服务器字段 ─────────────────────────────────────── + @Schema(description = "IP地址(服务器用)") + private String ip; + + // ─── 数据库字段 ─────────────────────────────────────── + @Schema(description = "数据库类型: MySQL/PostgreSQL/Redis/MongoDB(数据库用)") + private String dbType; + + @Schema(description = "连接主机地址(数据库用)") + private String host; + + @Schema(description = "连接端口(数据库用)") + private Integer port; + + // ─── 云存储字段 ─────────────────────────────────────── + @Schema(description = "地区/Region(云存储用)") + private String region; + + @Schema(description = "访问权限: public-read/private(云存储用)") + private String acl; + + @Schema(description = "已用空间(字节,云存储用)") + private Long usedBytes; + + // ─── 域名字段 ───────────────────────────────────────── + @Schema(description = "域名(域名用)") + private String domain; + + @Schema(description = "注册商(域名用)") + private String registrar; + + @Schema(description = "是否已备案(域名用)") + private Boolean icp; + + @Schema(description = "ICP备案号(域名用)") + private String icpNo; + + @Schema(description = "是否已绑定SSL(域名用,冗余)") + private Boolean sslBound; + + // ─── SSL证书字段 ────────────────────────────────────── + @Schema(description = "证书类型: DV/OV/EV(SSL用)") + private String certType; + + @Schema(description = "颁发机构(SSL用)") + private String issuer; + + // ─── 通用字段 ───────────────────────────────────────── + @Schema(description = "状态: running/stopped/expired/pending") + private String status; + + @Schema(description = "到期时间") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate expireAt; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "所属用户ID") + private Integer userId; + + @Schema(description = "租户ID") + private Integer tenantId; + + @Schema(description = "是否删除: 0否 1是") + private Integer deleted; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/AppResourceMapper.java b/src/main/java/com/gxwebsoft/app/mapper/AppResourceMapper.java new file mode 100644 index 0000000..94847b6 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/AppResourceMapper.java @@ -0,0 +1,35 @@ +package com.gxwebsoft.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.app.entity.AppResource; +import com.gxwebsoft.app.param.AppResourceParam; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 开发者资源 Mapper + * + * @author 科技小王子 + * @since 2026-03-31 + */ +public interface AppResourceMapper extends BaseMapper { + + /** + * 分页查询(关联应用名称) + */ + List selectPageRel(@Param("page") IPage page, + @Param("param") AppResourceParam param); + + /** + * 查询全部列表(关联应用名称) + */ + List selectListRel(@Param("param") AppResourceParam param); + + /** + * 统计各类型资源数量,返回 [{resourceType, cnt}] + */ + List> countByType(@Param("userId") Integer userId, + @Param("tenantId") Integer tenantId); +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppResourceMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppResourceMapper.xml new file mode 100644 index 0000000..292b82c --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppResourceMapper.xml @@ -0,0 +1,75 @@ + + + + + + + SELECT a.*, w.website_name + FROM app_resource a + LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0 + + a.deleted = 0 + + AND a.resource_id = #{param.resourceId} + + + AND a.resource_type = #{param.resourceType} + + + AND a.website_id = #{param.websiteId} + + + AND a.provider = #{param.provider} + + + AND a.status = #{param.status} + + + AND a.user_id = #{param.userId} + + + AND a.tenant_id = #{param.tenantId} + + + AND ( + a.name LIKE CONCAT('%', #{param.keywords}, '%') + OR a.ip LIKE CONCAT('%', #{param.keywords}, '%') + OR a.domain LIKE CONCAT('%', #{param.keywords}, '%') + OR a.host LIKE CONCAT('%', #{param.keywords}, '%') + ) + + + AND a.create_time >= #{param.createTimeStart} + + + AND a.create_time <= #{param.createTimeEnd} + + + ORDER BY a.create_time DESC + + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/app/param/AppResourceParam.java b/src/main/java/com/gxwebsoft/app/param/AppResourceParam.java new file mode 100644 index 0000000..bc18e73 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/param/AppResourceParam.java @@ -0,0 +1,54 @@ +package com.gxwebsoft.app.param; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.gxwebsoft.common.core.annotation.QueryField; +import com.gxwebsoft.common.core.annotation.QueryType; +import com.gxwebsoft.common.core.web.BaseParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 开发者资源查询参数 + * + * @author 科技小王子 + * @since 2026-03-31 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(name = "AppResourceParam对象", description = "开发者资源查询参数") +public class AppResourceParam extends BaseParam { + private static final long serialVersionUID = 1L; + + @Schema(description = "资源ID") + @QueryField(type = QueryType.EQ) + private Long resourceId; + + @Schema(description = "资源类型: server/database/storage/domain/ssl") + @QueryField(type = QueryType.EQ) + private String resourceType; + + @Schema(description = "关联应用ID") + @QueryField(type = QueryType.EQ) + private Long websiteId; + + @Schema(description = "服务商") + @QueryField(type = QueryType.EQ) + private String provider; + + @Schema(description = "状态") + @QueryField(type = QueryType.EQ) + private String status; + + @Schema(description = "所属用户ID") + @QueryField(type = QueryType.EQ) + private Integer userId; + + @Schema(description = "租户ID") + @QueryField(type = QueryType.EQ) + private Integer tenantId; + + @Schema(description = "关键词(名称/IP/域名/Host模糊搜索)") + private String keywords; +} diff --git a/src/main/java/com/gxwebsoft/app/service/AppResourceService.java b/src/main/java/com/gxwebsoft/app/service/AppResourceService.java new file mode 100644 index 0000000..76f4057 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/AppResourceService.java @@ -0,0 +1,39 @@ +package com.gxwebsoft.app.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.app.entity.AppResource; +import com.gxwebsoft.app.param.AppResourceParam; +import com.gxwebsoft.common.core.web.PageResult; + +import java.util.List; +import java.util.Map; + +/** + * 开发者资源 Service + * + * @author 科技小王子 + * @since 2026-03-31 + */ +public interface AppResourceService extends IService { + + /** 分页查询(关联应用名称) */ + PageResult pageRel(AppResourceParam param); + + /** 列表查询 */ + List listRel(AppResourceParam param); + + /** 详情(关联查询) */ + AppResource getByIdRel(Long resourceId); + + /** 新增资源 */ + AppResource addResource(AppResource resource, Integer userId); + + /** 更新资源 */ + AppResource updateResource(AppResource resource); + + /** 删除资源(逻辑删除) */ + void removeResource(Long resourceId, Integer userId); + + /** 按资源类型统计数量 [{resourceType, cnt}] */ + Map countByType(Integer userId, Integer tenantId); +} diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppResourceServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppResourceServiceImpl.java new file mode 100644 index 0000000..dc3aa96 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppResourceServiceImpl.java @@ -0,0 +1,109 @@ +package com.gxwebsoft.app.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.app.entity.AppResource; +import com.gxwebsoft.app.mapper.AppResourceMapper; +import com.gxwebsoft.app.param.AppResourceParam; +import com.gxwebsoft.app.service.AppResourceService; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 开发者资源 Service 实现 + * + * @author 科技小王子 + * @since 2026-03-31 + */ +@Slf4j +@Service +public class AppResourceServiceImpl extends ServiceImpl + implements AppResourceService { + + @Override + public PageResult pageRel(AppResourceParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("create_time desc"); + List list = baseMapper.selectPageRel(page, param); + return new PageResult<>(list, page.getTotal()); + } + + @Override + public List listRel(AppResourceParam param) { + return baseMapper.selectListRel(param); + } + + @Override + public AppResource getByIdRel(Long resourceId) { + AppResourceParam param = new AppResourceParam(); + param.setResourceId(resourceId); + List list = baseMapper.selectListRel(param); + return list.isEmpty() ? null : list.get(0); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public AppResource addResource(AppResource resource, Integer userId) { + resource.setUserId(userId); + resource.setDeleted(0); + resource.setCreateTime(LocalDateTime.now()); + resource.setUpdateTime(LocalDateTime.now()); + // 默认状态 + if (resource.getStatus() == null) { + resource.setStatus("running"); + } + save(resource); + log.info("新增资源成功, type={}, name={}, userId={}", resource.getResourceType(), resource.getName(), userId); + return resource; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public AppResource updateResource(AppResource resource) { + if (resource.getResourceId() == null) { + throw new RuntimeException("资源ID不能为空"); + } + resource.setUpdateTime(LocalDateTime.now()); + updateById(resource); + return getByIdRel(resource.getResourceId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeResource(Long resourceId, Integer userId) { + AppResource resource = getById(resourceId); + if (resource == null) { + throw new RuntimeException("资源不存在"); + } + if (!resource.getUserId().equals(userId)) { + throw new RuntimeException("无权操作此资源"); + } + resource.setDeleted(1); + resource.setUpdateTime(LocalDateTime.now()); + updateById(resource); + log.info("删除资源成功, resourceId={}, userId={}", resourceId, userId); + } + + @Override + public Map countByType(Integer userId, Integer tenantId) { + List> raw = baseMapper.countByType(userId, tenantId); + Map result = new HashMap<>(); + // 初始化所有类型为 0 + for (String type : new String[]{"server", "database", "storage", "domain", "ssl"}) { + result.put(type, 0L); + } + for (Map row : raw) { + String type = (String) row.get("resourceType"); + Long cnt = ((Number) row.get("cnt")).longValue(); + result.put(type, cnt); + } + return result; + } +} diff --git a/src/main/java/com/gxwebsoft/app/sql/app_resource.sql b/src/main/java/com/gxwebsoft/app/sql/app_resource.sql new file mode 100644 index 0000000..7f18c0b --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/sql/app_resource.sql @@ -0,0 +1,54 @@ +-- ---------------------------- +-- 开发者资源管理表 +-- 统一存放服务器/数据库/云存储/域名/SSL证书等开发基础设施资源 +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `app_resource` ( + `resource_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '资源ID', + `resource_type` VARCHAR(20) NOT NULL COMMENT '资源类型: server/database/storage/domain/ssl', + `name` VARCHAR(100) NOT NULL COMMENT '资源名称', + `provider` VARCHAR(30) DEFAULT NULL COMMENT '服务商: tencent/aliyun/huawei/other', + + -- 服务器字段 + `ip` VARCHAR(50) DEFAULT NULL COMMENT 'IP地址(服务器用)', + + -- 数据库字段 + `db_type` VARCHAR(30) DEFAULT NULL COMMENT '数据库类型: MySQL/PostgreSQL/Redis/MongoDB', + `host` VARCHAR(200) DEFAULT NULL COMMENT '连接主机地址(数据库用)', + `port` INT DEFAULT NULL COMMENT '连接端口(数据库用)', + + -- 云存储字段 + `region` VARCHAR(50) DEFAULT NULL COMMENT '地区/Region(云存储用)', + `acl` VARCHAR(30) DEFAULT 'private' COMMENT '访问权限: public-read/private(云存储用)', + `used_bytes` BIGINT DEFAULT 0 COMMENT '已用空间(字节,云存储用)', + + -- 域名字段 + `domain` VARCHAR(200) DEFAULT NULL COMMENT '域名(域名/SSL用)', + `registrar` VARCHAR(100) DEFAULT NULL COMMENT '注册商(域名用)', + `icp` TINYINT(1) DEFAULT 0 COMMENT '是否已备案: 0否 1是(域名用)', + `icp_no` VARCHAR(100) DEFAULT NULL COMMENT 'ICP备案号(域名用)', + `ssl_bound` TINYINT(1) DEFAULT 0 COMMENT '是否已绑定SSL(域名冗余标记)', + + -- SSL证书字段 + `cert_type` VARCHAR(10) DEFAULT NULL COMMENT '证书类型: DV/OV/EV(SSL用)', + `issuer` VARCHAR(100) DEFAULT NULL COMMENT '颁发机构(SSL用)', + + -- 通用字段 + `status` VARCHAR(20) NOT NULL DEFAULT 'running' COMMENT '状态: running/stopped/expired/pending', + `website_id` BIGINT DEFAULT NULL COMMENT '关联应用ID(可选)', + `expire_at` DATE DEFAULT NULL COMMENT '到期时间', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `user_id` INT NOT NULL COMMENT '所属用户ID', + `tenant_id` INT DEFAULT NULL COMMENT '租户ID', + `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + + PRIMARY KEY (`resource_id`), + KEY `idx_resource_type` (`resource_type`), + KEY `idx_user_id` (`user_id`), + KEY `idx_tenant_id` (`tenant_id`), + KEY `idx_website_id` (`website_id`), + KEY `idx_status` (`status`), + KEY `idx_expire_at` (`expire_at`), + KEY `idx_deleted` (`deleted`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开发者资源管理(服务器/数据库/云存储/域名/SSL)'; From 5029be1f1f89c58c2de26f857e3289b9de910e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 1 Apr 2026 00:35:07 +0800 Subject: [PATCH 18/21] =?UTF-8?q?feat(ticket):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=B7=A5=E5=8D=95=E5=88=97=E8=A1=A8=E6=8E=92=E5=BA=8F=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现工单状态优先级排序:pending > assigned > processing > resolved > closed - 添加紧急程度优先级排序:urgent > high > normal > low - 保持相同状态下按创建时间倒序排列 - 使用FIELD函数优化数据库查询排序性能 - 移除原有的简单排序规则并替换为复合排序逻辑 --- .../gxwebsoft/app/service/impl/AppTicketServiceImpl.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java index 0e0e913..2df1d82 100644 --- a/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java @@ -81,7 +81,8 @@ public class AppTicketServiceImpl extends ServiceImpl q.like(AppTicket::getTitle, param.getKeywords()) .or().like(AppTicket::getTicketNo, param.getKeywords())) - .orderByDesc(AppTicket::getCreateTime); + // 状态优先级:pending > assigned > processing > resolved > closed;同状态内最新的排前 + .last("ORDER BY FIELD(status,'pending','assigned','processing','resolved','closed','rejected'), create_time DESC"); baseMapper.selectPage(page, wrapper); return new PageResult<>(page.getRecords(), page.getTotal()); @@ -105,8 +106,8 @@ public class AppTicketServiceImpl extends ServiceImpl q.like(AppTicket::getTitle, param.getKeywords()) .or().like(AppTicket::getTicketNo, param.getKeywords())) - .orderByAsc(AppTicket::getStatus) // pending 排前面 - .orderByDesc(AppTicket::getCreateTime); + // 状态优先级 > 紧急程度 > 最新提交时间 + .last("ORDER BY FIELD(status,'pending','assigned','processing','resolved','closed','rejected'), FIELD(priority,'urgent','high','normal','low'), create_time DESC"); baseMapper.selectPage(page, wrapper); return new PageResult<>(page.getRecords(), page.getTotal()); From f1dde97538cd9ae6fbdc565774348e9262b3e1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 1 Apr 2026 00:59:06 +0800 Subject: [PATCH 19/21] =?UTF-8?q?feat(website):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=8F=91=E5=B8=83=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在CmsWebsite实体中添加发布状态、定价模式、应用描述等相关字段 - 实现应用上架审核流程:提交审核、撤回申请、下架、管理员审批等功能 - 添加审核列表分页查询接口,支持按发布状态筛选 - 更新路由配置,调整开发者资源路径 - 添加数据库表结构变更SQL脚本,增加发布管理相关字段 --- .../app/controller/AppResourceController.java | 2 +- .../cms/controller/CmsWebsiteController.java | 81 +++++++++++++ .../com/gxwebsoft/cms/entity/CmsWebsite.java | 44 +++++++ .../gxwebsoft/cms/param/CmsWebsiteParam.java | 3 + .../cms/service/CmsWebsiteService.java | 22 ++++ .../service/impl/CmsWebsiteServiceImpl.java | 110 +++++++++++++++++- .../gxwebsoft/cms/sql/cms_website_publish.sql | 17 +++ 7 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/gxwebsoft/cms/sql/cms_website_publish.sql diff --git a/src/main/java/com/gxwebsoft/app/controller/AppResourceController.java b/src/main/java/com/gxwebsoft/app/controller/AppResourceController.java index 35a8273..28fa031 100644 --- a/src/main/java/com/gxwebsoft/app/controller/AppResourceController.java +++ b/src/main/java/com/gxwebsoft/app/controller/AppResourceController.java @@ -26,7 +26,7 @@ import java.util.Map; @Slf4j @Tag(name = "开发者资源管理") @RestController -@RequestMapping("/api/_modules/app/developer-resource") +@RequestMapping("/api/app/developer-resource") public class AppResourceController extends BaseController { @Resource diff --git a/src/main/java/com/gxwebsoft/cms/controller/CmsWebsiteController.java b/src/main/java/com/gxwebsoft/cms/controller/CmsWebsiteController.java index 0a1ade6..ddf42de 100644 --- a/src/main/java/com/gxwebsoft/cms/controller/CmsWebsiteController.java +++ b/src/main/java/com/gxwebsoft/cms/controller/CmsWebsiteController.java @@ -562,4 +562,85 @@ public class CmsWebsiteController extends BaseController { return success("清除成功"); } + // ─── 发布管理 ──────────────────────────────────────────────────────── + + @Operation(summary = "提交上架审核") + @PostMapping("/submitReview") + public ApiResult submitReview(@RequestBody Map body) { + Integer websiteId = body.get("websiteId") instanceof Number ? ((Number) body.get("websiteId")).intValue() : null; + if (websiteId == null) return fail("websiteId 不能为空"); + String priceType = (String) body.get("priceType"); + Integer price = body.get("price") instanceof Number ? ((Number) body.get("price")).intValue() : 0; + String subscriptionPeriod = (String) body.get("subscriptionPeriod"); + String appDescription = (String) body.get("appDescription"); + String detailDescription = (String) body.get("detailDescription"); + String screenshots = (String) body.get("screenshots"); + Integer userId = getLoginUserId(); + try { + cmsWebsiteService.submitReview(websiteId, priceType, price, subscriptionPeriod, + appDescription, detailDescription, screenshots, userId); + return success("上架申请提交成功,等待审核"); + } catch (Exception e) { + return fail(e.getMessage()); + } + } + + @Operation(summary = "撤回审核申请") + @PostMapping("/withdrawReview/{websiteId}") + public ApiResult withdrawReview(@PathVariable Integer websiteId) { + try { + cmsWebsiteService.withdrawReview(websiteId, getLoginUserId()); + return success("已撤回审核申请"); + } catch (Exception e) { + return fail(e.getMessage()); + } + } + + @Operation(summary = "下架应用") + @PostMapping("/unpublish/{websiteId}") + public ApiResult unpublish(@PathVariable Integer websiteId) { + try { + cmsWebsiteService.unpublish(websiteId, getLoginUserId()); + return success("已下架"); + } catch (Exception e) { + return fail(e.getMessage()); + } + } + + @PreAuthorize("hasAuthority('cms:website:update')") + @Operation(summary = "管理员审核通过") + @PostMapping("/approveReview/{websiteId}") + public ApiResult approveReview(@PathVariable Integer websiteId) { + try { + cmsWebsiteService.approveReview(websiteId, getLoginUserId()); + return success("审核通过,应用已上架"); + } catch (Exception e) { + return fail(e.getMessage()); + } + } + + @PreAuthorize("hasAuthority('cms:website:update')") + @Operation(summary = "管理员审核拒绝") + @PostMapping("/rejectReview") + public ApiResult rejectReview(@RequestBody Map body) { + Integer websiteId = body.get("websiteId") instanceof Number ? ((Number) body.get("websiteId")).intValue() : null; + String rejectReason = (String) body.get("rejectReason"); + if (websiteId == null) return fail("websiteId 不能为空"); + if (rejectReason == null || rejectReason.isBlank()) return fail("请填写拒绝原因"); + try { + cmsWebsiteService.rejectReview(websiteId, rejectReason, getLoginUserId()); + return success("已拒绝该应用上架申请"); + } catch (Exception e) { + return fail(e.getMessage()); + } + } + + @PreAuthorize("hasAuthority('cms:website:update')") + @Operation(summary = "获取审核列表(管理员)") + @GetMapping("/pageReviews") + public ApiResult> pageReviews(CmsWebsiteParam param) { + return success(cmsWebsiteService.pageReviews(param)); + } + } + diff --git a/src/main/java/com/gxwebsoft/cms/entity/CmsWebsite.java b/src/main/java/com/gxwebsoft/cms/entity/CmsWebsite.java index 31087c4..7f253b4 100644 --- a/src/main/java/com/gxwebsoft/cms/entity/CmsWebsite.java +++ b/src/main/java/com/gxwebsoft/cms/entity/CmsWebsite.java @@ -321,6 +321,50 @@ public class CmsWebsite implements Serializable { @TableField(exist = false) private CmsWebsiteSetting setting; + // ─── 发布管理字段 ────────────────────────────────────────────────── + + @Schema(description = "发布状态: developing开发中 pending_review待审核 published已上架 rejected审核未通过 deprecated已下架") + private String publishStatus; + + @Schema(description = "定价模式: free免费 one_time一次性 subscription订阅") + private String priceType; + + @Schema(description = "订阅周期: month按月 year按年") + private String subscriptionPeriod; + + @Schema(description = "应用简介(市场展示用)") + private String appDescription; + + @Schema(description = "详细说明(富文本)") + private String detailDescription; + + @Schema(description = "应用截图(JSON数组字符串)") + private String screenshots; + + @Schema(description = "安装/使用次数") + private Integer installCount; + + @Schema(description = "评分(1-5)") + private java.math.BigDecimal rating; + + @Schema(description = "审核拒绝原因") + private String rejectReason; + + @Schema(description = "提交审核时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private java.util.Date publishApplyTime; + + @Schema(description = "正式发布上架时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private java.util.Date publishTime; + + @Schema(description = "审核人用户ID") + private Integer reviewerId; + + @Schema(description = "审核操作时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private java.util.Date reviewTime; + public String getPhone(){ return DesensitizedUtil.mobilePhone(this.phone); } diff --git a/src/main/java/com/gxwebsoft/cms/param/CmsWebsiteParam.java b/src/main/java/com/gxwebsoft/cms/param/CmsWebsiteParam.java index 6236415..7c7be19 100644 --- a/src/main/java/com/gxwebsoft/cms/param/CmsWebsiteParam.java +++ b/src/main/java/com/gxwebsoft/cms/param/CmsWebsiteParam.java @@ -225,4 +225,7 @@ public class CmsWebsiteParam extends BaseParam { @QueryField(type = QueryType.EQ) private String adminPhone; + @Schema(description = "发布状态筛选: developing/pending_review/published/rejected/deprecated") + private String publishStatus; + } diff --git a/src/main/java/com/gxwebsoft/cms/service/CmsWebsiteService.java b/src/main/java/com/gxwebsoft/cms/service/CmsWebsiteService.java index a78dfa2..db72d5d 100644 --- a/src/main/java/com/gxwebsoft/cms/service/CmsWebsiteService.java +++ b/src/main/java/com/gxwebsoft/cms/service/CmsWebsiteService.java @@ -67,4 +67,26 @@ public interface CmsWebsiteService extends IService { * @param tenantId 租户ID */ void clearSiteInfoCache(Integer tenantId); + + // ─── 发布管理 ────────────────────────────────────────────────── + + /** 提交上架审核 */ + void submitReview(Integer websiteId, String priceType, Integer price, + String subscriptionPeriod, String appDescription, + String detailDescription, String screenshots, Integer userId); + + /** 撤回审核申请(回到开发中) */ + void withdrawReview(Integer websiteId, Integer userId); + + /** 下架应用 */ + void unpublish(Integer websiteId, Integer userId); + + /** 管理员审核通过 */ + void approveReview(Integer websiteId, Integer reviewerId); + + /** 管理员审核拒绝 */ + void rejectReview(Integer websiteId, String rejectReason, Integer reviewerId); + + /** 分页查询审核列表(管理员) */ + PageResult pageReviews(CmsWebsiteParam param); } diff --git a/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java b/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java index 8b74f83..c3a6868 100644 --- a/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java +++ b/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java @@ -417,9 +417,113 @@ public class CmsWebsiteServiceImpl extends ServiceImpl pageReviews(CmsWebsiteParam param) { + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = + new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(param.getCurrent(), param.getSize() > 0 ? param.getSize() : 20); + com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper wrapper = + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(CmsWebsite::getDeleted, 0) + .isNotNull(CmsWebsite::getPublishStatus) + .ne(CmsWebsite::getPublishStatus, "developing") + .eq(cn.hutool.core.util.ObjectUtil.isNotEmpty(param.getPublishStatus()), + CmsWebsite::getPublishStatus, param.getPublishStatus()) + .and(cn.hutool.core.util.ObjectUtil.isNotEmpty(param.getKeywords()), q -> + q.like(CmsWebsite::getWebsiteName, param.getKeywords()) + .or().like(CmsWebsite::getWebsiteCode, param.getKeywords())) + .last("ORDER BY FIELD(publish_status,'pending_review','rejected','published','deprecated'), publish_apply_time DESC"); + baseMapper.selectPage(page, wrapper); + return new com.gxwebsoft.common.core.web.PageResult<>(page.getRecords(), page.getTotal()); + } + + private CmsWebsite getWebsiteFromDatabase(Integer tenantId) { return getByTenantId(tenantId); } diff --git a/src/main/java/com/gxwebsoft/cms/sql/cms_website_publish.sql b/src/main/java/com/gxwebsoft/cms/sql/cms_website_publish.sql new file mode 100644 index 0000000..a70f593 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/sql/cms_website_publish.sql @@ -0,0 +1,17 @@ +-- 为 cms_website 表新增发布管理相关字段 +-- 执行前请确认已连接正确的数据库 + +ALTER TABLE `cms_website` + ADD COLUMN `publish_status` VARCHAR(20) NULL DEFAULT 'developing' COMMENT '发布状态: developing开发中 pending_review待审核 published已上架 rejected审核未通过 deprecated已下架' AFTER `market`, + ADD COLUMN `price_type` VARCHAR(20) NULL COMMENT '定价模式: free免费 one_time一次性 subscription订阅' AFTER `publish_status`, + ADD COLUMN `subscription_period` VARCHAR(10) NULL COMMENT '订阅周期: month按月 year按年' AFTER `price_type`, + ADD COLUMN `app_description` VARCHAR(500) NULL COMMENT '应用简介(市场展示用)' AFTER `subscription_period`, + ADD COLUMN `detail_description` TEXT NULL COMMENT '详细说明(富文本)' AFTER `app_description`, + ADD COLUMN `screenshots` TEXT NULL COMMENT '应用截图(JSON数组字符串)' AFTER `detail_description`, + ADD COLUMN `install_count` INT NULL DEFAULT 0 COMMENT '安装/使用次数' AFTER `screenshots`, + ADD COLUMN `rating` DECIMAL(3,1) NULL DEFAULT 0.0 COMMENT '评分(1-5)' AFTER `install_count`, + ADD COLUMN `reject_reason` VARCHAR(500) NULL COMMENT '审核拒绝原因' AFTER `rating`, + ADD COLUMN `publish_apply_time` DATETIME NULL COMMENT '提交审核时间' AFTER `reject_reason`, + ADD COLUMN `publish_time` DATETIME NULL COMMENT '正式发布上架时间' AFTER `publish_apply_time`, + ADD COLUMN `reviewer_id` INT NULL COMMENT '审核人用户ID' AFTER `publish_time`, + ADD COLUMN `review_time` DATETIME NULL COMMENT '审核操作时间' AFTER `reviewer_id`; From 8a9d779d08849ad373f2a1d47ffefd69b10efca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 1 Apr 2026 00:59:44 +0800 Subject: [PATCH 20/21] =?UTF-8?q?feat(website):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=8F=91=E5=B8=83=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在CmsWebsite实体中添加发布状态、定价模式、应用描述等相关字段 - 实现应用上架审核流程:提交审核、撤回申请、下架、管理员审批等功能 - 添加审核列表分页查询接口,支持按发布状态筛选 - 更新路由配置,调整开发者资源路径 - 添加数据库表结构变更SQL脚本,增加发布管理相关字段 --- .../com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java index 2df1d82..f351c76 100644 --- a/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppTicketServiceImpl.java @@ -8,7 +8,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.app.entity.AppTicket; import com.gxwebsoft.app.entity.AppTicketReply; import com.gxwebsoft.app.entity.AppUser; From 84cd2142774d7501e54f755911155240d32056c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 1 Apr 2026 01:04:49 +0800 Subject: [PATCH 21/21] =?UTF-8?q?feat(website):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=8F=91=E5=B8=83=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在CmsWebsite实体中添加发布状态、定价模式、应用描述等相关字段 - 实现应用上架审核流程:提交审核、撤回申请、下架、管理员审批等功能 - 添加审核列表分页查询接口,支持按发布状态筛选 - 更新路由配置,调整开发者资源路径 - 添加数据库表结构变更SQL脚本,增加发布管理相关字段 --- .../com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java b/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java index c3a6868..c0aeb0e 100644 --- a/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java +++ b/src/main/java/com/gxwebsoft/cms/service/impl/CmsWebsiteServiceImpl.java @@ -507,7 +507,7 @@ public class CmsWebsiteServiceImpl extends ServiceImpl pageReviews(CmsWebsiteParam param) { com.baomidou.mybatisplus.extension.plugins.pagination.Page page = - new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(param.getCurrent(), param.getSize() > 0 ? param.getSize() : 20); + new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(param.getPage() != null ? param.getPage() : 1, param.getLimit() != null && param.getLimit() > 0 ? param.getLimit() : 20); com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper wrapper = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(CmsWebsite::getDeleted, 0)