diff --git a/design-qa.md b/design-qa.md index a102897..535ec1d 100644 --- a/design-qa.md +++ b/design-qa.md @@ -37,4 +37,129 @@ - None required for the requested scope. +## 2026-08-28 工具栏样式修正复核 + +- Source visual truth: `C:\Users\mayn\AppData\Local\Temp\codex-clipboard-73344a5b-ced8-4cca-952c-84eb3ad96fdd.png` +- Pre-fix implementation screenshot: `C:\Users\mayn\AppData\Local\Temp\codex-clipboard-97abadfa-922a-4b9e-90e6-bd7d0d40e611.png` +- Revised implementation screenshot: unavailable because the local Java web service is not currently listening on a browser-accessible port. +- Viewport/state: desktop project edit page, group-required switch enabled. +- Full-view evidence: the pre-fix screenshot shows the controls stacked at the table's left edge, while the source places them horizontally at the upper-right edge. +- Focused region evidence: source order is `分组必选 → 开关 → 配置分组选择数量`; the pre-fix implementation wraps the button onto a second row. + +### Findings and fixes + +- [Fixed in source, pending rendered confirmation] P1 toolbar placement: moved the controls into an explicitly loaded flex toolbar with right alignment. +- [Fixed in source, pending rendered confirmation] P2 button styling: added the reference-like 40 px height, light-blue background, blue border, icon/text color and hover state. +- [Fixed in source, pending rendered confirmation] P2 table rhythm: removed the 20 px gap so the toolbar connects directly to the table. +- Typography: retains the project's existing Element UI typography and 14 px action text. +- Colors: uses the project's Element UI primary-blue family. +- Images/assets: no raster assets are involved; the existing Element UI setting icon is retained. +- Copy/content: all requested Chinese labels remain unchanged. + +### Comparison history + +1. User-provided pre-fix capture identified the invalid `/deep/` component selectors and vertically wrapped controls. +2. Replaced those selectors and added the effective page-level stylesheet used by this server-rendered Vue page. +3. JavaScript syntax and Git whitespace checks passed; post-fix browser capture remains unavailable until the local application is started or refreshed. + +## 2026-08-28 卡片背景与圆角按钮复核 + +- Source visual truth: `C:\Users\mayn\AppData\Local\Temp\codex-clipboard-095c2fbf-eaaf-46a4-953b-9d1070638bdf.png` +- Revised implementation screenshot: unavailable because the local Java web service is not listening on a browser-accessible port. +- Viewport/state: desktop welfare-project edit page with the welfare-options section visible. +- Full-view evidence: the reference groups the toolbar, table and add action within one white elevated card. +- Focused region evidence: the reference uses a subtle card radius/shadow and a compact blue add button with rounded corners. + +### Findings and fixes + +- [Fixed in source, pending rendered confirmation] P2 card hierarchy: added a 1 px neutral border, 6 px radius and low-contrast shadow to the welfare-option card. +- [Fixed in source, pending rendered confirmation] P2 spacing rhythm: added 14 px horizontal/bottom card padding and a 10 px gap above the footer action. +- [Fixed in source, pending rendered confirmation] P2 button shape: added a dedicated 6 px radius to the add-welfare-option button while retaining Element UI primary states. +- Typography: unchanged and inherited from the existing Element UI design system. +- Colors: white card, `#ebeef5` border and existing primary-blue button match the source hierarchy. +- Images/assets: this style correction does not require new raster assets. +- Copy/content: `添加福利选项` and all existing labels remain unchanged. + +### Comparison history + +1. The supplied reference identified the missing soft card treatment and rounded add button. +2. Added semantic card/footer/button classes and matching page-loaded CSS. +3. JavaScript syntax and Git whitespace checks passed; post-fix browser capture remains unavailable until the local application is started or refreshed. + +final result: blocked + +# 2026-08-28 福利评价设计验收 + +- Source visual truth: `C:\Users\mayn\AppData\Local\Temp\codex-clipboard-c7cafe9d-913d-4ba3-acb4-f1190c14cd78.png`、`C:\Users\mayn\AppData\Local\Temp\codex-clipboard-bbaf6b8d-5f27-40bd-b2d4-fdcb0d5b9f22.png`、`C:\Users\mayn\AppData\Local\Temp\codex-clipboard-1f0c70c6-cd0f-4369-a82e-12345e61447b.png` +- Implementation screenshots: `F:\coding\zhgh_zjvtit_v4\target\codex-preview\welfare-evaluation-desktop-v2.png`、`F:\coding\zhgh_zjvtit_v4\target\codex-preview\welfare-evaluation-mobile-v5.png` +- Comparison images: `F:\coding\zhgh_zjvtit_v4\target\codex-preview\compare-evaluation-desktop-v2.png`、`F:\coding\zhgh_zjvtit_v4\target\codex-preview\compare-evaluation-mobile-v5.png` +- Viewports: desktop 1400 × 900 CSS px;mobile 420 × 938 CSS px;device scale factor 1. +- Density normalization: the 840 × 1875 mobile reference was normalized to 420 × 938 before side-by-side comparison. +- State: evaluation dialog/sheet open; three selected benefits; every score initially zero; remark empty. + +## Findings + +- No remaining P0/P1/P2 visual mismatch in the requested evaluation dialog and bottom sheet. +- Desktop: title, information prompt, two-column benefit table, five-star control, zero-score prompt, 200-character remark and footer actions match the reference hierarchy. +- Mobile: the existing hero/card design is retained; the evaluation action, bottom rounded sheet, drag handle, close action, benefit rows and bottom actions align with the supplied mobile reference. +- Typography and colors inherit the project's Element UI/Vant design systems; no unrelated visual tokens or custom raster assets were introduced. +- Copy includes the selected quantity for every benefit and consistently uses `评价(5星制)`、`备注`、`取消` and `提交评价`. + +## Interaction Evidence + +- The third star in the first mobile row was clicked and exposed `aria-checked=true`, confirming one-star-per-point selection behavior. +- The default state renders all items with zero stars and an empty remark. +- The PC and mobile production scripts passed JavaScript syntax checks during implementation; the Java backend passed an offline Maven compile. +- The local visual harness recorded no blocking render error. Database-backed API submission still requires the supplied SQL to be executed in the target environment before end-to-end testing. + +## Comparison History + +1. The first mobile pass produced an evaluation sheet that was visibly taller than the reference. +2. Reduced header margins, table row height and footer height, then restored the reference drag handle. +3. The final sheet begins at the same vertical position as the normalized reference and preserves all actions without page-level scrolling. +4. The PC zero-score state was corrected to explicitly display `请点击评分`, matching the reference. + +## Follow-up Polish + +- None required for the requested visual scope. + +final result: passed + +# 2026-08-28 移动端“去评价”按钮设计验收 + +- Source visual truth: `C:\Users\mayn\AppData\Local\Temp\codex-clipboard-fd28cb5c-c88c-4c99-8789-4b5083bc812c.png` +- Implementation screenshot: `F:\coding\zhgh_zjvtit_v4\target\codex-preview\welfare-evaluate-button-final.png` +- Full-view comparison: `F:\coding\zhgh_zjvtit_v4\target\codex-preview\compare-welfare-evaluate-button-final.png` +- Focused comparison: `F:\coding\zhgh_zjvtit_v4\target\codex-preview\compare-welfare-evaluate-button-focus-final.png` +- Viewport: 431 × 911 CSS px,device scale factor 1. +- Source pixels: 862 × 1822;implementation pixels: 431 × 911;source was normalized to 431 × 911 before comparison. +- State: “我的福利”卡片已显示,评价弹层关闭,评价入口处于默认可点击状态。 + +## Findings + +- No remaining P0/P1/P2 mismatch in the requested evaluation-button region. +- Fonts and typography: `去评价` uses the existing Vant/project font stack at 14 px and matches the reference's compact action hierarchy. +- Spacing and layout rhythm: the implementation uses a 34 px button height, 17 px pill radius, 13 px horizontal padding and 6 px icon gap; the resulting width matches the normalized reference closely. +- Colors and visual tokens: border, icon and copy reuse the existing `#1989fa` primary blue on a white background. +- Image quality and assets: the icon uses Vant's real `star` font icon; no SVG, CSS drawing, emoji or raster placeholder was introduced. +- Copy and content: the label is `去评价`, matching the supplied reference. + +## Interaction Evidence + +- Clicking `去评价` opened the welfare-evaluation bottom sheet. +- The button remained visible and accessible as a semantic button. +- Browser console errors: 0. +- JavaScript syntax and Git whitespace checks passed. + +## Comparison History + +1. Initial implementation used `comment-o`、`评价` and a 6 px rounded rectangle, which differed from the supplied target. +2. Replaced the icon with the closest matching existing Vant `star` icon, changed the copy to `去评价`, and applied the pill treatment. +3. Focused comparison found the first 97 px implementation width slightly wider than the approximately 93 px normalized target; horizontal padding and icon size were reduced. +4. Final implementation matches the target size, border, color, copy and overall visual weight without an actionable P0/P1/P2 issue. + +## Follow-up Polish + +- P3: the reference star contains a small inner highlight while the closest existing Vant `star` glyph is solid; retaining the project icon library avoids introducing a custom asset for this minor difference. + final result: passed diff --git a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareEvaluationManageController.java b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareEvaluationManageController.java new file mode 100644 index 0000000..d241b98 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareEvaluationManageController.java @@ -0,0 +1,90 @@ +package com.budwk.app.zhgh.welfare.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.result.Result; +import com.budwk.app.zhgh.welfare.param.WelfareEvaluationDetail; +import com.budwk.app.zhgh.welfare.param.WelfareEvaluationManagePageForm; +import com.budwk.app.zhgh.welfare.service.WelfareUserEvaluationService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.util.Collections; + +/** + * PC端福利评分管理。Controller只接收参数、执行基础校验并调用评价服务。 + */ +@IocBean +@Ok("json:full") +@At("/platform/welfare/evaluation/manage") +@Api(tags = "福利评分管理") +public class WelfareEvaluationManageController { + + @Inject + private WelfareUserEvaluationService welfareUserEvaluationService; + + @At("") + @Ok("beetl:/platform/zhgh/welfare/evaluationManage/index.html") + @SaCheckPermission("welfare.evaluation.manage") + public void index() { + } + + /** + * 分页查询项目福利名单的评分汇总。 + * + * @param pageForm 年度、福利项目ID及分页排序参数 + * @return JSON分页结果;每个人员只返回一行,综合评价为逐项福利评分平均值 + */ + @At + @SaCheckPermission("welfare.evaluation.manage") + @ApiOperation("分页查询福利评分") + public Result pageData(@Valid @Param("pageForm") WelfareEvaluationManagePageForm pageForm) { + if (StrUtil.isBlank(pageForm.getProjectId())) { + return Result.success(new Pagination<>(1, pageForm.getPageSize(), 0, Collections.emptyList())); + } + return Result.success(welfareUserEvaluationService.managePageData(pageForm)); + } + + /** + * 查询指定人员在项目中的逐项福利评分。 + * + * @param projectId 福利项目ID + * @param userId 福利名单人员ID + * @return JSON详情,items为纵向福利评分,remark为统一评价备注 + */ + @At + @SaCheckPermission("welfare.evaluation.manage") + @ApiOperation("查看人员福利评价") + public Result detail(String projectId, String userId) { + if (StrUtil.isBlank(projectId) || StrUtil.isBlank(userId)) { + return Result.error("评价查询参数不完整"); + } + WelfareEvaluationDetail detail = welfareUserEvaluationService.getManageEvaluationDetail(projectId, userId); + return Result.success(detail); + } + + /** + * 导出项目评分明细。人员按项目和人员ID去重,项目全部福利选项动态生成评分列。 + * + * @param projectId 福利项目ID + * @param response Excel文件响应 + */ + @At + @Ok("void") + @SaCheckPermission("welfare.evaluation.manage") + @ApiOperation("导出福利评分") + public void exportXlsx(String projectId, HttpServletResponse response) { + if (StrUtil.isBlank(projectId)) { + return; + } + welfareUserEvaluationService.exportManageXlsx(projectId, response); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareMineController.java b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareMineController.java index 70abf43..2a05517 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareMineController.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareMineController.java @@ -58,9 +58,10 @@ public class WelfareMineController { WHEN wus.selectUserId IS NOT NULL THEN 1 ELSE 0 END AS isChoose, - GROUP_CONCAT(DISTINCT wuso.optionName ,'(',wus.selectNum,'份)') AS gist_list, - MAX(wus.selectTime) AS selectTime, - wus.receiveAddress, + GROUP_CONCAT(DISTINCT wuso.optionName ,'(',wus.selectNum,'份)') AS gist_list, + MAX(wus.selectTime) AS selectTime, + MAX(wus.deliveryDate) AS deliveryDate, + wus.receiveAddress, wl.userId FROM welfare_list wl @@ -97,6 +98,8 @@ public class WelfareMineController { wpus.receiveAddress, wpso.optionName, wpus.selectNum, + wpus.deliveryDate, + wpus.remark, wpus.userSign, wpus.courierNumber, wpus.selectTime, diff --git a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareSelectionSituationController.java b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareSelectionSituationController.java index c682e76..397efcf 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareSelectionSituationController.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareSelectionSituationController.java @@ -69,6 +69,30 @@ public class WelfareSelectionSituationController { return Result.success(pagination); } + /** + * 按选择情况页面查询条件发送钉钉消息。 + * + * @param pageForm 页面当前查询条件,后台会据此重新查询全部符合权限范围的人员 + * @param messageContent 前端已去除富文本样式的纯文本消息内容 + * @return JSON结果;成功消息中包含实际发送人数,失败时返回具体原因 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SLog(type = "welfare", tag = "选择情况", msg = "发送钉钉消息") + @SaCheckPermission("welfare.selection.situation") + @ApiOperation("根据查询条件发送钉钉消息") + public Result sendMessage(@Valid @Param("pageForm") WelfareSelectionSituationPageForm pageForm, + @Param("messageContent") String messageContent) { + if (StrUtil.isBlank(pageForm.getProjectId())) { + return Result.error("请选择福利项目"); + } + if (StrUtil.isBlank(messageContent)) { + return Result.error("消息内容不能为空"); + } + int receiverCount = situationService.sendDingTalkMessage(pageForm, messageContent); + return Result.success("消息发送成功,共发送" + receiverCount + "人"); + } + @At @SaCheckPermission("welfare.selection.situation") @@ -89,17 +113,29 @@ public class WelfareSelectionSituationController { return Result.error("此用户没有选择的权限"); } + Date selectTime = new Date(); + String additionalInfoValidationMessage = welfareProjectService.validateSelectionAdditionalInfo(selections, selectTime); + if (additionalInfoValidationMessage != null) { + return Result.error(additionalInfoValidationMessage); + } + String defaultOptionValidationMessage = welfareProjectService.validateSystemDefaultOptionSelection(projectId, selections); if (defaultOptionValidationMessage != null) { return Result.error(defaultOptionValidationMessage); } + // 代选与用户自选使用同一套分组份数规则,校验失败时不覆盖原有选择记录。 + String groupQuantityValidationMessage = welfareProjectService.validateGroupSelectionQuantity(projectId, selections); + if (groupQuantityValidationMessage != null) { + return Result.error(groupQuantityValidationMessage); + } + //删除上次选择的 dao.clear(WelfareUserSelection.class, Cnd.where(WelfareUserSelection::getWelfareId, "=", projectId).and(WelfareUserSelection::getSelectUserId, "=", userId)); for (WelfareUserSelection welfareUserSelection : selections) { welfareUserSelection.setWelfareId(projectId); welfareUserSelection.setSelectUserId(userId); - welfareUserSelection.setSelectTime(new Date()); + welfareUserSelection.setSelectTime(selectTime); } dao.insert(selections); return Result.success("选择成功"); diff --git a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserEvaluationController.java b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserEvaluationController.java new file mode 100644 index 0000000..2c9083d --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserEvaluationController.java @@ -0,0 +1,70 @@ +package com.budwk.app.zhgh.welfare.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.result.Result; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.welfare.param.WelfareEvaluationDetail; +import com.budwk.app.zhgh.welfare.param.WelfareEvaluationForm; +import com.budwk.app.zhgh.welfare.service.WelfareUserEvaluationService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +/** + * 福利用户评价接口,供PC“选择福利”和移动端“我的福利”共同调用。 + */ +@IocBean +@Ok("json:full") +@At("/platform/welfare/evaluation") +@Api(tags = "福利用户评价") +public class WelfareUserEvaluationController { + + @Inject + private WelfareUserEvaluationService welfareUserEvaluationService; + + /** + * 获取当前登录用户在指定福利项目中的评价回显数据。 + * + * @param projectId 福利项目ID + * @return 当前项目全部福利选择和评分 + */ + @At + @SaCheckPermission(value = {"welfare.user.select", "welfare.mine"}, mode = SaMode.OR) + @ApiOperation("获取福利评价") + public Result getEvaluation(String projectId) { + String userId = SecurityUtil.getUserId(); + String availableValidationMessage = welfareUserEvaluationService.validateEvaluationAvailable(projectId, userId); + if (availableValidationMessage != null) { + return Result.error(availableValidationMessage); + } + WelfareEvaluationDetail detail = welfareUserEvaluationService.getEvaluationDetail(projectId, userId); + return Result.success(detail); + } + + /** + * 保存当前登录用户对项目中全部已选福利的评价。 + * + * @param evaluationForm 福利评价表单JSON + * @return 保存结果 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SLog(type = "welfare", tag = "福利评价", msg = "提交福利评价") + @SaCheckPermission(value = {"welfare.user.select", "welfare.mine"}, mode = SaMode.OR) + @ApiOperation("保存福利评价") + public Result saveEvaluation(@Param("evaluationForm") WelfareEvaluationForm evaluationForm) { + String validationMessage = welfareUserEvaluationService.saveEvaluation(evaluationForm, SecurityUtil.getUserId()); + if (validationMessage != null) { + return Result.error(validationMessage); + } + return Result.success("评价提交成功"); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserSelectController.java b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserSelectController.java index 6bf72b6..6fa9360 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserSelectController.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserSelectController.java @@ -57,6 +57,8 @@ public class WelfareUserSelectController { ELSE 0 END AS isChoose, wus.selectTime, + MAX(wus.deliveryDate) AS deliveryDate, + MAX(wus.remark) AS remark, GROUP_CONCAT(DISTINCT wuso.optionName ,'(',wus.selectNum,'份)') AS gist_list, wus.receiveAddress FROM @@ -93,11 +95,23 @@ public class WelfareUserSelectController { } WelfareProject project = dao.fetch(WelfareProject.class, projectId); + Date selectTime = new Date(); + String additionalInfoValidationMessage = welfareProjectService.validateSelectionAdditionalInfo(selections, selectTime); + if (additionalInfoValidationMessage != null) { + return Result.error(additionalInfoValidationMessage); + } + String defaultOptionValidationMessage = welfareProjectService.validateSystemDefaultOptionSelection(projectId, selections); if (defaultOptionValidationMessage != null) { return Result.error(defaultOptionValidationMessage); } + // 后端统一校验各分组累计份数,防止 PC 或移动端绕过前端限制提交。 + String groupQuantityValidationMessage = welfareProjectService.validateGroupSelectionQuantity(projectId, selections); + if (groupQuantityValidationMessage != null) { + return Result.error(groupQuantityValidationMessage); + } + int sum = Arrays.stream(selections).mapToInt(selection -> Objects.requireNonNullElse(selection.getSelectNum(), 0)).sum(); if (sum > project.getMultiSelectNum()) { return Result.error("选择的数量不能超过" + project.getMultiSelectNum()); @@ -109,7 +123,7 @@ public class WelfareUserSelectController { for (WelfareUserSelection welfareUserSelection : selections) { welfareUserSelection.setWelfareId(projectId); welfareUserSelection.setSelectUserId(SecurityUtil.getUserId()); - welfareUserSelection.setSelectTime(new Date()); + welfareUserSelection.setSelectTime(selectTime); } dao.insert(selections); return Result.success("选择成功"); diff --git a/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareProjectSubject.java b/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareProjectSubject.java index e0361bc..9045281 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareProjectSubject.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareProjectSubject.java @@ -42,7 +42,7 @@ public class WelfareProjectSubject extends BaseModel implements Serializable { @Column @Comment("题目类型") - @ColDefine(type = ColType.VARCHAR, width = 10) + @ColDefine(type = ColType.VARCHAR, width = 32) private String subjectType; @Column diff --git a/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareUserEvaluation.java b/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareUserEvaluation.java new file mode 100644 index 0000000..6af5144 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareUserEvaluation.java @@ -0,0 +1,69 @@ +package com.budwk.app.zhgh.welfare.model; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.ColDefine; +import org.nutz.dao.entity.annotation.ColType; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.dao.entity.annotation.Index; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.entity.annotation.TableIndexes; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; +import java.util.Date; + +/** + * 福利用户评价。每条记录关联一条福利人员选择记录,一颗星对应一分。 + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@Table("welfare_user_evaluation") +@Comment("福利用户评价") +@TableIndexes({ + @Index(name = "uk_welfare_user_evaluation_selection", fields = {"selectionId"}, unique = true), + @Index(name = "idx_welfare_user_evaluation_project_user", fields = {"projectId", "userId"}, unique = false) +}) +public class WelfareUserEvaluation extends BaseModel implements Serializable { + + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("福利人员选择记录ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String selectionId; + + @Column + @Comment("福利项目ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String projectId; + + @Column + @Comment("评价用户ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String userId; + + @Column + @Comment("评分,范围1至5") + @ColDefine(type = ColType.INT) + private Integer score; + + @Column + @Comment("评价备注") + @ColDefine(type = ColType.VARCHAR, width = 200) + private String remark; + + @Column + @Comment("评价时间") + @ColDefine(type = ColType.DATETIME) + private Date evaluatedAt; +} diff --git a/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareUserSelection.java b/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareUserSelection.java index 0ae808c..6b5a487 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareUserSelection.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/model/WelfareUserSelection.java @@ -60,6 +60,16 @@ public class WelfareUserSelection extends BaseModel implements Serializable { @ColDefine(type = ColType.DATETIME) private Date selectTime; + @Column + @Comment("配送时间") + @ColDefine(type = ColType.DATE) + private Date deliveryDate; + + @Column + @Comment("选择备注") + @ColDefine(type = ColType.VARCHAR, width = 200) + private String remark; + @Column @Comment("收货地址") @ColDefine(type = ColType.VARCHAR, width = 100) diff --git a/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareEvaluationDetail.java b/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareEvaluationDetail.java new file mode 100644 index 0000000..48aa70c --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareEvaluationDetail.java @@ -0,0 +1,47 @@ +package com.budwk.app.zhgh.welfare.param; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 福利评价回显数据。 + */ +@Data +public class WelfareEvaluationDetail { + + /** 福利项目ID。 */ + private String projectId; + + /** 已保存的统一评价备注。 */ + private String remark; + + /** 是否已经完整评价当前项目的全部已选福利。 */ + private Boolean evaluated; + + /** 当前用户在项目中的福利选择及评分。 */ + private List items = new ArrayList<>(); + + /** + * 福利评价回显明细。 + */ + @Data + public static class EvaluationItem { + + /** 福利人员选择记录ID。 */ + private String selectionId; + + /** 福利选项ID。 */ + private String optionId; + + /** 福利名称。 */ + private String optionName; + + /** 用户选择的福利份数。 */ + private Integer selectNum; + + /** 星级评分,未评价时为0。 */ + private Integer score; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareEvaluationForm.java b/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareEvaluationForm.java new file mode 100644 index 0000000..9778683 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareEvaluationForm.java @@ -0,0 +1,34 @@ +package com.budwk.app.zhgh.welfare.param; + +import lombok.Data; + +import java.util.List; + +/** + * 福利评价提交参数。 + */ +@Data +public class WelfareEvaluationForm { + + /** 福利项目ID。 */ + private String projectId; + + /** 本次福利评价的统一备注,最多200字。 */ + private String remark; + + /** 当前项目全部已选福利的评分明细。 */ + private List items; + + /** + * 单条福利选择的评分参数。 + */ + @Data + public static class EvaluationItem { + + /** 福利人员选择记录ID。 */ + private String selectionId; + + /** 星级评分,一颗星为一分,取值范围1至5。 */ + private Integer score; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareEvaluationManagePageForm.java b/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareEvaluationManagePageForm.java new file mode 100644 index 0000000..d11d514 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareEvaluationManagePageForm.java @@ -0,0 +1,24 @@ +package com.budwk.app.zhgh.welfare.param; + +import com.budwk.app.base.param.PageForm; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 福利评分管理分页查询参数。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ApiModel("福利评分管理分页参数") +public class WelfareEvaluationManagePageForm extends PageForm { + + /** 用于前端联动福利项目列表的年度。 */ + @ApiModelProperty("年度") + private Integer year; + + /** 当前查询的福利项目ID。 */ + @ApiModelProperty("福利项目ID") + private String projectId; +} diff --git a/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareProjectService.java b/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareProjectService.java index 389a53a..e68311c 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareProjectService.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareProjectService.java @@ -4,6 +4,7 @@ import com.budwk.app.base.service.BaseService; import com.budwk.app.zhgh.welfare.model.WelfareProject; import com.budwk.app.zhgh.welfare.model.WelfareUserSelection; +import java.util.Date; import java.util.List; public interface WelfareProjectService extends BaseService { @@ -33,6 +34,25 @@ public interface WelfareProjectService extends BaseService { */ String validateSystemDefaultOptionSelection(String projectId, WelfareUserSelection[] selections); + /** + * 按福利选项分组累计每项选择份数,并校验项目配置的最小、最大选择数量。 + * + * @param projectId 福利项目 ID,用于查询分组开关、分组配置及选项所属分组 + * @param selections 用户提交的福利选择数组,每项份数通过 selectNum 累计到对应分组 + * @return 校验通过返回 {@code null};不满足分组数量限制时返回具体错误提示 + */ + String validateGroupSelectionQuantity(String projectId, WelfareUserSelection[] selections); + + /** + * 校验福利选择附加信息。配送时间为必填项,必须按日期粒度不早于本次选择时间; + * 同一次提交的全部福利项必须使用相同配送时间和备注。 + * + * @param selections 用户提交的福利选择数组,每项需包含 deliveryDate,可选包含 remark + * @param selectTime 后端生成的本次选择时间,用于防止客户端伪造日期 + * @return 校验通过返回 {@code null},否则返回可直接展示的错误提示 + */ + String validateSelectionAdditionalInfo(WelfareUserSelection[] selections, Date selectTime); + /** * 保存项目信息 * @param project diff --git a/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareSelectionSituationService.java b/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareSelectionSituationService.java index 21aefde..3fa93b2 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareSelectionSituationService.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareSelectionSituationService.java @@ -11,6 +11,15 @@ public interface WelfareSelectionSituationService extends BaseService { + + /** + * 校验当前用户是否已到福利配送日期。只有存在选择记录、全部记录已设置配送日期,且当前日期 + * 不早于配送日期时才允许打开或提交评价。 + * + * @param projectId 福利项目ID + * @param userId 当前登录用户ID + * @return 允许评价返回{@code null},否则返回可直接展示的错误提示 + */ + String validateEvaluationAvailable(String projectId, String userId); + + /** + * 查询指定用户在福利项目中的全部选择及评价,供PC弹窗和移动端底部弹层回显。 + * + * @param projectId 福利项目ID + * @param userId 当前登录用户ID + * @return 当前选择记录、福利名称、份数和已保存评分 + */ + WelfareEvaluationDetail getEvaluationDetail(String projectId, String userId); + + /** + * 校验并保存当前项目全部已选福利的评分。重复提交时按选择记录更新评价。 + * + * @param form 福利评价表单,必须包含当前项目全部选择记录 + * @param userId 当前登录用户ID,用于校验选择记录归属 + * @return 保存成功返回{@code null},失败返回可直接展示的错误提示 + */ + String saveEvaluation(WelfareEvaluationForm form, String userId); + + /** + * 分页查询福利项目名单中的人员评分,一个人员只返回一条汇总记录。 + * + * @param pageForm 项目ID及分页排序条件 + * @return 人员分页数据,包含基础信息、综合评价和评价备注 + */ + Pagination managePageData(WelfareEvaluationManagePageForm pageForm); + + /** + * 查询管理端指定人员的逐项福利评分,并校验当前管理员的数据范围。 + * + * @param projectId 福利项目ID + * @param userId 福利名单人员ID + * @return 逐项福利评分、选择份数、综合评价所需明细及统一备注 + */ + WelfareEvaluationDetail getManageEvaluationDetail(String projectId, String userId); + + /** + * 按项目和人员去重导出福利评分,每个人员一行,项目全部福利选项动态生成评分列。 + * + * @param projectId 福利项目ID + * @param response Excel下载响应 + */ + void exportManageXlsx(String projectId, HttpServletResponse response); +} diff --git a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareProjectServiceImpl.java b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareProjectServiceImpl.java index edbf0a9..aa6154a 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareProjectServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareProjectServiceImpl.java @@ -103,6 +103,126 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl i return "系统默认福利“" + String.join("、", missingOptionNames) + "”不可取消"; } + /** + * 校验用户在每个分组中选择的总份数。分组总份数为该分组下所有已选项 selectNum 的累计值, + * 最大值为 0 时表示不限制;分组为空的福利选项不参与校验。 + * + * @param projectId 福利项目 ID + * @param selections 当前提交的福利选择数据 + * @return 校验通过返回 {@code null},否则返回包含分组和当前份数的错误提示 + */ + @Override + public String validateGroupSelectionQuantity(String projectId, WelfareUserSelection[] selections) { + WelfareProject project = fetch(projectId); + if (project == null) { + return "福利项目不存在"; + } + if (!Boolean.TRUE.equals(project.getGroupRequired())) { + return null; + } + + WelfareProjectSubject configSubject = dao().fetch(WelfareProjectSubject.class, + Cnd.where(WelfareProjectSubject::getProjectId, "=", projectId) + .and(WelfareProjectSubject::getSubjectType, "=", GROUP_CONFIG_SUBJECT_TYPE)); + List configs = configSubject == null + || configSubject.getGroupSelectionConfigs() == null + ? Collections.emptyList() + : configSubject.getGroupSelectionConfigs(); + if (configs.isEmpty()) { + return null; + } + + // 选项所属分组必须以后端数据为准,禁止通过提交参数伪造或绕过分组限制。 + Map optionMap = dao().query(WelfareProjectSubjectOption.class, + Cnd.where(WelfareProjectSubjectOption::getWelfareId, "=", projectId)) + .stream() + .collect(Collectors.toMap(WelfareProjectSubjectOption::getId, option -> option, (left, right) -> left)); + Map groupQuantityMap = new HashMap<>(); + WelfareUserSelection[] submittedSelections = selections == null ? new WelfareUserSelection[0] : selections; + for (WelfareUserSelection selection : submittedSelections) { + if (selection == null) { + continue; + } + int selectNum = Objects.requireNonNullElse(selection.getSelectNum(), 0); + if (selectNum < 0) { + return "福利选择份数不能小于0"; + } + if (selectNum == 0) { + continue; + } + WelfareProjectSubjectOption option = optionMap.get(selection.getSelectOptionId()); + if (option == null) { + return "提交数据中存在不属于当前项目的福利选项"; + } + if (StrUtil.isBlank(option.getGroupName())) { + continue; + } + groupQuantityMap.merge(option.getGroupName().trim(), (long) selectNum, Long::sum); + } + + // 逐个配置分组校验累计份数,未选择的分组按 0 份处理。 + for (WelfareProjectSubject.GroupSelectionConfig config : configs) { + if (config == null || StrUtil.isBlank(config.getGroupName())) { + continue; + } + String groupName = config.getGroupName().trim(); + long selectedQuantity = groupQuantityMap.getOrDefault(groupName, 0L); + int minSelectNum = Objects.requireNonNullElse(config.getMinSelectNum(), 1); + int maxSelectNum = Objects.requireNonNullElse(config.getMaxSelectNum(), 0); + if (selectedQuantity < minSelectNum) { + return "分组“" + groupName + "”至少需要选择" + minSelectNum + + "份,当前已选择" + selectedQuantity + "份"; + } + if (maxSelectNum > 0 && selectedQuantity > maxSelectNum) { + return "分组“" + groupName + "”最多可选择" + maxSelectNum + + "份,当前已选择" + selectedQuantity + "份"; + } + } + return null; + } + + /** + * 校验一次选择提交中的配送日期和备注。日期比较统一截断到当天零点,允许选择当天配送; + * 多项福利的附加信息必须一致,避免同一次选择产生互相冲突的评价开放时间。 + * + * @param selections 当前提交的福利选择数据 + * @param selectTime 后端生成的选择时间 + * @return 校验通过返回 {@code null},否则返回错误提示 + */ + @Override + public String validateSelectionAdditionalInfo(WelfareUserSelection[] selections, Date selectTime) { + if (selections == null || selections.length == 0) { + return "请至少选择一项福利"; + } + Date selectionDate = DateUtil.beginOfDay(selectTime == null ? new Date() : selectTime); + Date submittedDeliveryDate = null; + String submittedRemark = null; + for (WelfareUserSelection selection : selections) { + if (selection == null || selection.getDeliveryDate() == null) { + return "请选择配送时间"; + } + Date deliveryDate = DateUtil.beginOfDay(selection.getDeliveryDate()); + if (deliveryDate.before(selectionDate)) { + return "配送时间不能早于选择时间"; + } + if (submittedDeliveryDate == null) { + submittedDeliveryDate = deliveryDate; + submittedRemark = StrUtil.trim(selection.getRemark()); + } else if (!submittedDeliveryDate.equals(deliveryDate)) { + return "同一次选择的配送时间必须一致"; + } else if (!Objects.equals(submittedRemark, StrUtil.trim(selection.getRemark()))) { + return "同一次选择的备注必须一致"; + } + String remark = StrUtil.trim(selection.getRemark()); + if (remark != null && remark.length() > 200) { + return "备注不能超过200字"; + } + selection.setDeliveryDate(deliveryDate); + selection.setRemark(remark); + } + return null; + } + private void fillOptionRank(String projectId, List options) { if (options == null || options.isEmpty()) { return; diff --git a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareSelectionSituationServiceImpl.java b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareSelectionSituationServiceImpl.java index 1c5e0a2..1915a2b 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareSelectionSituationServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareSelectionSituationServiceImpl.java @@ -7,9 +7,12 @@ import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; import cn.afterturn.easypoi.excel.export.ExcelExportService; import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HtmlUtil; import com.budwk.app.base.constant.RoleConstant; +import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.page.Pagination; import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.base.sms.SmsService; import com.budwk.app.base.utils.CommonDownloadUtil; import com.budwk.app.base.utils.PageUtil; import com.budwk.app.sys.models.Sys_file; @@ -29,6 +32,8 @@ import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.lang.util.NutMap; @@ -51,6 +56,9 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl loginNames = listMap(sql).stream() + .map(item -> item.getString("loginName")) + .filter(StrUtil::isNotBlank) + .distinct() + .toList(); + if (loginNames.isEmpty()) { + throw new BaseException("当前查询条件下没有可发送消息的人员"); + } + + String title = project.getName() + "-福利通知"; + boolean success = smsService.sendMsg("6", loginNames, null, title, plainContent, null, null); + if (!success) { + throw new BaseException("钉钉消息发送失败,请检查消息发送配置或稍后重试"); + } + return loginNames.size(); + } + + /** + * 构建选择情况列表与消息接收人共用的筛选条件。 + * 除页面条件外会主动叠加当前登录人的分工会数据范围。 + * + * @param pageForm 页面查询条件 + * @return 可用于选择情况关联查询的Nutz条件对象 + */ + private Cnd buildSituationCondition(WelfareSelectionSituationPageForm pageForm) { + Cnd cnd = Cnd.NEW(); + if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_SPORTS.name())) { + cnd.and("t1.welfareUnionId", "=", SecurityUtil.getUnionId()); + } + + cnd.and("t1.projectId", "=", pageForm.getProjectId()); + cnd.andEX("t2.selectOptionId", "=", pageForm.getWelfareOptionId()); + cnd.andEX("t4.aidFundMemberUserType", "=", pageForm.getAidFundMemberUserType()); + cnd.andEX("t4.userAttribute", "=", pageForm.getUserAttribute()); + if (StrUtil.isNotBlank(pageForm.getUserName())) { + cnd.where().andLike("t4.username", pageForm.getUserName()); + } + if (StrUtil.isNotBlank(pageForm.getLoginName())) { + cnd.where().andLike("t4.loginname", pageForm.getLoginName()); + } + cnd.andEX("t1.welfareUnitId", "in", pageForm.getUnitIds()); + cnd.andEX("t1.welfareUnionId", "=", pageForm.getUnionId()); + if (pageForm.getIsSelect() != null) { + cnd.and("t2.id", pageForm.getIsSelect() ? "IS NOT" : "IS", null); + } + return cnd; + } + @Override public void exportXlsx(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response) { Sql sql = Sqls.create(""" diff --git a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareUserEvaluationServiceImpl.java b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareUserEvaluationServiceImpl.java new file mode 100644 index 0000000..1c5e8c1 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareUserEvaluationServiceImpl.java @@ -0,0 +1,445 @@ +package com.budwk.app.zhgh.welfare.service.impl; + +import cn.afterturn.easypoi.excel.ExcelExportUtil; +import cn.afterturn.easypoi.excel.entity.ExportParams; +import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; +import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.constant.RoleConstant; +import com.budwk.app.base.exception.BaseException; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.base.utils.PageUtil; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.welfare.model.WelfareList; +import com.budwk.app.zhgh.welfare.model.WelfareProject; +import com.budwk.app.zhgh.welfare.model.WelfareProjectSubjectOption; +import com.budwk.app.zhgh.welfare.model.WelfareUserEvaluation; +import com.budwk.app.zhgh.welfare.model.WelfareUserSelection; +import com.budwk.app.zhgh.welfare.param.WelfareEvaluationDetail; +import com.budwk.app.zhgh.welfare.param.WelfareEvaluationForm; +import com.budwk.app.zhgh.welfare.param.WelfareEvaluationManagePageForm; +import com.budwk.app.zhgh.welfare.service.WelfareUserEvaluationService; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Workbook; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; + +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 福利用户评价服务实现。 + */ +@IocBean(args = {"refer:dao"}) +@Slf4j +public class WelfareUserEvaluationServiceImpl extends BaseServiceImpl implements WelfareUserEvaluationService { + + public WelfareUserEvaluationServiceImpl(Dao dao) { + super(dao); + } + + /** + * 评价开放时间以后端福利选择记录为准,避免客户端隐藏按钮后仍可直接调用评价接口。 + * 配送时间按日期粒度比较,到达配送日期当天即可评价。 + * + * @param projectId 福利项目ID + * @param userId 当前登录用户ID + * @return 允许评价返回{@code null},否则返回错误提示 + */ + @Override + public String validateEvaluationAvailable(String projectId, String userId) { + if (StrUtil.isBlank(projectId) || StrUtil.isBlank(userId)) { + return "评价参数不完整"; + } + List selections = dao().query(WelfareUserSelection.class, + Cnd.where(WelfareUserSelection::getWelfareId, "=", projectId) + .and(WelfareUserSelection::getSelectUserId, "=", userId)); + if (selections.isEmpty()) { + return "当前项目暂无可评价的福利选择"; + } + Date currentDate = DateUtil.beginOfDay(new Date()); + for (WelfareUserSelection selection : selections) { + if (selection.getDeliveryDate() == null) { + return "当前福利尚未设置配送时间,暂不能评价"; + } + if (currentDate.before(DateUtil.beginOfDay(selection.getDeliveryDate()))) { + return "配送时间未到,暂不能评价"; + } + } + return null; + } + + /** + * 查询当前用户在项目中的福利选择,并合并已保存的星级与备注。 + * + * @param projectId 福利项目ID + * @param userId 当前登录用户ID + * @return 可直接供评价弹窗回显的数据 + */ + @Override + public WelfareEvaluationDetail getEvaluationDetail(String projectId, String userId) { + WelfareEvaluationDetail detail = new WelfareEvaluationDetail(); + detail.setProjectId(projectId); + detail.setRemark(""); + + if (StrUtil.isBlank(projectId) || StrUtil.isBlank(userId)) { + detail.setEvaluated(false); + return detail; + } + + List selections = dao().query(WelfareUserSelection.class, + Cnd.where(WelfareUserSelection::getWelfareId, "=", projectId) + .and(WelfareUserSelection::getSelectUserId, "=", userId) + .asc(WelfareUserSelection::getSelectTime)); + if (selections.isEmpty()) { + detail.setEvaluated(false); + return detail; + } + + Set optionIds = selections.stream() + .map(WelfareUserSelection::getSelectOptionId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + Map optionMap = optionIds.isEmpty() + ? Collections.emptyMap() + : dao().query(WelfareProjectSubjectOption.class, Cnd.where(WelfareProjectSubjectOption::getId, "in", optionIds)) + .stream() + .collect(Collectors.toMap(WelfareProjectSubjectOption::getId, Function.identity(), (left, right) -> left)); + Map evaluationMap = dao().query(WelfareUserEvaluation.class, + Cnd.where(WelfareUserEvaluation::getProjectId, "=", projectId) + .and(WelfareUserEvaluation::getUserId, "=", userId)) + .stream() + .collect(Collectors.toMap(WelfareUserEvaluation::getSelectionId, Function.identity(), (left, right) -> left)); + + boolean evaluated = true; + for (WelfareUserSelection selection : selections) { + WelfareProjectSubjectOption option = optionMap.get(selection.getSelectOptionId()); + WelfareUserEvaluation evaluation = evaluationMap.get(selection.getId()); + WelfareEvaluationDetail.EvaluationItem item = new WelfareEvaluationDetail.EvaluationItem(); + item.setSelectionId(selection.getId()); + item.setOptionId(selection.getSelectOptionId()); + item.setOptionName(option == null ? "未知福利" : option.getOptionName()); + item.setSelectNum(Objects.requireNonNullElse(selection.getSelectNum(), 0)); + item.setScore(evaluation == null ? 0 : Objects.requireNonNullElse(evaluation.getScore(), 0)); + detail.getItems().add(item); + + if (evaluation == null || item.getScore() < 1 || item.getScore() > 5) { + evaluated = false; + } else if (StrUtil.isBlank(detail.getRemark()) && StrUtil.isNotBlank(evaluation.getRemark())) { + // 统一备注会写入每条评价记录,回显时读取首个非空值即可。 + detail.setRemark(evaluation.getRemark()); + } + } + detail.setEvaluated(evaluated); + return detail; + } + + /** + * 保存福利评价。服务端通过当前选择记录集合校验提交项,避免评价他人或伪造选择ID。 + * + * @param form 福利评价提交数据 + * @param userId 当前登录用户ID + * @return 保存成功返回{@code null},校验失败返回提示 + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public String saveEvaluation(WelfareEvaluationForm form, String userId) { + if (form == null || StrUtil.isBlank(form.getProjectId()) || StrUtil.isBlank(userId)) { + return "评价参数不完整"; + } + String availableValidationMessage = validateEvaluationAvailable(form.getProjectId(), userId); + if (availableValidationMessage != null) { + return availableValidationMessage; + } + String remark = StrUtil.trim(form.getRemark()); + if (remark != null && remark.length() > 200) { + return "评价备注不能超过200字"; + } + + List selections = dao().query(WelfareUserSelection.class, + Cnd.where(WelfareUserSelection::getWelfareId, "=", form.getProjectId()) + .and(WelfareUserSelection::getSelectUserId, "=", userId)); + if (selections.isEmpty()) { + return "当前项目暂无可评价的福利选择"; + } + List submittedItems = form.getItems() == null + ? Collections.emptyList() + : form.getItems(); + if (submittedItems.size() != selections.size()) { + return "请为本次选择的每项福利完成评分"; + } + + Map submittedItemMap = new HashMap<>(); + for (WelfareEvaluationForm.EvaluationItem item : submittedItems) { + if (item == null || StrUtil.isBlank(item.getSelectionId()) || item.getScore() == null + || item.getScore() < 1 || item.getScore() > 5) { + return "每项福利都需要选择1至5星评分"; + } + if (submittedItemMap.put(item.getSelectionId(), item) != null) { + return "评价数据中存在重复的福利选择记录"; + } + } + + Set currentSelectionIds = selections.stream().map(WelfareUserSelection::getId).collect(Collectors.toSet()); + if (!currentSelectionIds.equals(new HashSet<>(submittedItemMap.keySet()))) { + return "评价数据与当前福利选择不一致,请刷新后重试"; + } + + Map existingEvaluationMap = dao().query(WelfareUserEvaluation.class, + Cnd.where(WelfareUserEvaluation::getSelectionId, "in", currentSelectionIds)) + .stream() + .collect(Collectors.toMap(WelfareUserEvaluation::getSelectionId, Function.identity(), (left, right) -> left)); + Date evaluatedAt = new Date(); + for (WelfareUserSelection selection : selections) { + WelfareEvaluationForm.EvaluationItem submittedItem = submittedItemMap.get(selection.getId()); + WelfareUserEvaluation evaluation = existingEvaluationMap.get(selection.getId()); + if (evaluation == null) { + evaluation = new WelfareUserEvaluation() + .setSelectionId(selection.getId()) + .setProjectId(form.getProjectId()) + .setUserId(userId) + .setScore(submittedItem.getScore()) + .setRemark(remark) + .setEvaluatedAt(evaluatedAt); + dao().insert(evaluation); + } else { + // 重复评价只更新业务字段,selectionId、projectId和userId始终保持首次校验后的归属关系。 + evaluation.setScore(submittedItem.getScore()); + evaluation.setRemark(remark); + evaluation.setEvaluatedAt(evaluatedAt); + dao().update(evaluation); + } + } + return null; + } + + /** + * 管理端评分列表以福利名单为人员范围,并按项目和人员ID分组,确保人员选择多个福利时只显示一行。 + * 综合评价只统计已保存的逐项评分,未评价人员返回空值。 + * + * @param pageForm 项目ID及分页排序条件 + * @return 去重后的人员评分分页数据 + */ + @Override + public Pagination managePageData(WelfareEvaluationManagePageForm pageForm) { + Sql sql = Sqls.create(""" + SELECT + MIN(wl.id) AS id, + wl.projectId, + wl.userId, + u.loginname AS loginName, + u.username AS userName, + wl.welfareUnitName, + wl.welfareUnionName, + wl.userState, + wl.personType, + ROUND(AVG(e.score), 1) AS comprehensiveScore, + CASE + WHEN COUNT(e.id) = 0 THEN '未评价' + WHEN MAX(NULLIF(e.remark, '')) IS NULL THEN '已评价' + ELSE MAX(e.remark) + END AS evaluation + FROM welfare_list wl + LEFT JOIN sys_user u ON u.id = wl.userId + LEFT JOIN welfare_user_evaluation e ON e.projectId = wl.projectId AND e.userId = wl.userId + $condition + """); + Cnd cnd = buildManageCondition(pageForm.getProjectId()); + cnd.groupBy("wl.projectId", "wl.userId"); + if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { + cnd.asc("wl.welfareUnionName"); + cnd.asc("wl.welfareUnitName"); + cnd.asc("u.loginname"); + } else { + cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); + } + sql.setCondition(cnd); + return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + } + + /** + * 管理端查看评价前先确认人员属于项目福利名单及当前管理员可访问的分工会范围。 + * + * @param projectId 福利项目ID + * @param userId 福利名单人员ID + * @return 逐项福利评分详情 + */ + @Override + public WelfareEvaluationDetail getManageEvaluationDetail(String projectId, String userId) { + Cnd accessCnd = Cnd.where(WelfareList::getProjectId, "=", projectId) + .and(WelfareList::getUserId, "=", userId); + if (hasBranchUnionDataScope()) { + accessCnd.and(WelfareList::getWelfareUnionId, "=", SecurityUtil.getUnionId()); + } + if (dao().count(WelfareList.class, accessCnd) == 0) { + throw new BaseException("人员不在当前福利项目或无权查看该评价"); + } + return getEvaluationDetail(projectId, userId); + } + + /** + * 导出项目人员的福利评分。名单按项目和人员去重,每个人员占一行,项目全部福利选项 + * 按排序动态生成评分列;已选择未评价显示“未评价”,未选择的福利列留空。 + * + * @param projectId 福利项目ID + * @param response Excel下载响应 + */ + @Override + public void exportManageXlsx(String projectId, HttpServletResponse response) { + WelfareProject project = dao().fetch(WelfareProject.class, projectId); + if (project == null) { + throw new BaseException("福利项目不存在"); + } + + // 当前选项通过 welfareId 直接归属项目,同时兼容历史数据中仅通过 subjectId 关联项目的记录。 + Sql optionSql = Sqls.create(""" + SELECT DISTINCT + optionInfo.id, + optionInfo.optionName, + optionInfo.optionSort + FROM welfare_project_subject_option optionInfo + LEFT JOIN welfare_project_subject subjectInfo ON subjectInfo.id = optionInfo.subjectId + WHERE optionInfo.welfareId = @projectId OR subjectInfo.projectId = @projectId + ORDER BY optionInfo.optionSort, optionInfo.id + """); + optionSql.setParam("projectId", projectId); + List options = listMap(optionSql); + + Sql personSql = Sqls.create(""" + SELECT + project.name AS projectName, + wl.projectId, + wl.userId, + u.loginname AS loginName, + u.username AS userName, + wl.welfareUnitName, + wl.welfareUnionName, + evaluationInfo.comprehensiveScore, + COALESCE(evaluationInfo.evaluation, '未评价') AS evaluation + FROM welfare_list wl + INNER JOIN ( + SELECT MIN(id) AS id + FROM welfare_list + GROUP BY projectId, userId + ) uniqueList ON uniqueList.id = wl.id + INNER JOIN welfare_project project ON project.id = wl.projectId + LEFT JOIN sys_user u ON u.id = wl.userId + LEFT JOIN ( + SELECT + projectId, + userId, + ROUND(AVG(score), 1) AS comprehensiveScore, + CASE + WHEN COUNT(id) = 0 THEN '未评价' + WHEN MAX(NULLIF(remark, '')) IS NULL THEN '已评价' + ELSE MAX(remark) + END AS evaluation + FROM welfare_user_evaluation + GROUP BY projectId, userId + ) evaluationInfo ON evaluationInfo.projectId = wl.projectId AND evaluationInfo.userId = wl.userId + $condition + """); + Cnd cnd = buildManageCondition(projectId); + cnd.asc("wl.welfareUnionName"); + cnd.asc("wl.welfareUnitName"); + cnd.asc("u.loginname"); + personSql.setCondition(cnd); + List rows = listMap(personSql); + + Sql scoreSql = Sqls.create(""" + SELECT + selectionInfo.welfareId AS projectId, + selectionInfo.selectUserId AS userId, + selectionInfo.selectOptionId AS optionId, + MAX(evaluation.score) AS score, + COUNT(evaluation.id) AS evaluationCount + FROM welfare_project_user_selection selectionInfo + LEFT JOIN welfare_user_evaluation evaluation ON evaluation.selectionId = selectionInfo.id + WHERE selectionInfo.welfareId = @projectId + GROUP BY selectionInfo.welfareId, selectionInfo.selectUserId, selectionInfo.selectOptionId + """); + scoreSql.setParam("projectId", projectId); + List scores = listMap(scoreSql); + Map personMap = rows.stream().collect(Collectors.toMap( + row -> row.getString("projectId") + "::" + row.getString("userId"), + Function.identity(), + (left, right) -> left)); + for (NutMap score : scores) { + String personKey = score.getString("projectId") + "::" + score.getString("userId"); + NutMap person = personMap.get(personKey); + if (person != null && StrUtil.isNotBlank(score.getString("optionId"))) { + String optionKey = "optionScore_" + score.getString("optionId"); + Object scoreValue = score.getInt("evaluationCount", 0) > 0 ? score.get("score") : "未评价"; + person.put(optionKey, scoreValue); + } + } + + List entities = new ArrayList<>(); + entities.add(new ExcelExportEntity("项目名称", "projectName", 30)); + entities.add(new ExcelExportEntity("工号", "loginName", 18)); + entities.add(new ExcelExportEntity("姓名", "userName", 18)); + entities.add(new ExcelExportEntity("单位", "welfareUnitName", 30)); + entities.add(new ExcelExportEntity("工会", "welfareUnionName", 25)); + for (NutMap option : options) { + entities.add(new ExcelExportEntity(option.getString("optionName"), + "optionScore_" + option.getString("id"), 18)); + } + entities.add(new ExcelExportEntity("综合评价", "comprehensiveScore", 15)); + entities.add(new ExcelExportEntity("评价", "evaluation", 40)); + + ExportParams exportParams = new ExportParams(); + exportParams.setType(ExcelType.XSSF); + try (Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, rows)) { + CommonDownloadUtil.download(project.getName() + "评分列表.xlsx", workbook, response); + } catch (Exception e) { + log.error("导出福利评分列表失败,projectId={}", projectId, e); + throw new BaseException("导出福利评分列表失败"); + } + } + + /** + * 构建管理列表和导出共用的数据范围条件。 + * + * @param projectId 福利项目ID + * @return 包含项目范围和分工会角色数据权限的查询条件 + */ + private Cnd buildManageCondition(String projectId) { + Cnd cnd = Cnd.where("wl.projectId", "=", projectId); + if (hasBranchUnionDataScope()) { + cnd.and("wl.welfareUnionId", "=", SecurityUtil.getUnionId()); + } + return cnd; + } + + /** + * 判断当前用户是否只能查看所属分工会的数据。 + * + * @return 分工会主席、管理员或文体委员返回true + */ + private boolean hasBranchUnionDataScope() { + return AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_SPORTS.name()); + } + +} diff --git a/src/main/resources/db/welfare_user_evaluation_create.sql b/src/main/resources/db/welfare_user_evaluation_create.sql new file mode 100644 index 0000000..3f585b1 --- /dev/null +++ b/src/main/resources/db/welfare_user_evaluation_create.sql @@ -0,0 +1,21 @@ +-- 福利评价按人员选择记录逐项保存,一颗星对应一分。 +CREATE TABLE IF NOT EXISTS `welfare_user_evaluation` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `selectionId` varchar(32) NOT NULL COMMENT '福利人员选择记录ID', + `projectId` varchar(32) NOT NULL COMMENT '福利项目ID', + `userId` varchar(32) NOT NULL COMMENT '评价用户ID', + `score` int NOT NULL COMMENT '评分,范围1至5', + `remark` varchar(200) DEFAULT NULL COMMENT '评价备注', + `evaluatedAt` datetime DEFAULT NULL COMMENT '评价时间', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_welfare_user_evaluation_selection` (`selectionId`), + KEY `idx_welfare_user_evaluation_project_user` (`projectId`, `userId`), + CONSTRAINT `fk_welfare_user_evaluation_selection` + FOREIGN KEY (`selectionId`) REFERENCES `welfare_project_user_selection` (`id`) ON DELETE CASCADE, + CONSTRAINT `chk_welfare_user_evaluation_score` CHECK (`score` BETWEEN 1 AND 5) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='福利用户评价'; diff --git a/src/main/resources/views/platform/zhgh/welfare/evaluationManage/index.html b/src/main/resources/views/platform/zhgh/welfare/evaluationManage/index.html new file mode 100644 index 0000000..808843a --- /dev/null +++ b/src/main/resources/views/platform/zhgh/welfare/evaluationManage/index.html @@ -0,0 +1,296 @@ + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + 导出 + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + 以下为该人员本次选择的福利评价内容。 +
+ + + + + + + + + + +
+
备注
+ + +
+
+
+
+
+ + + + diff --git a/src/main/resources/views/platform/zhgh/welfare/include/fullHeightList.css b/src/main/resources/views/platform/zhgh/welfare/include/fullHeightList.css new file mode 100644 index 0000000..7135be7 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/welfare/include/fullHeightList.css @@ -0,0 +1,41 @@ +/* 福利列表页占满主内容区,禁止数据量增大时撑出整页纵向滚动条。 */ +.welfare-full-height-page { + display: flex; + flex-direction: column; + height: calc(100vh - 84px); + min-height: 0; + overflow: hidden; +} + +.welfare-full-height-page .welfare-search-card { + flex: 0 0 auto; +} + +.welfare-full-height-page .welfare-list-card { + flex: 1 1 0; + min-height: 0; +} + +/* 列表卡片内部使用纵向弹性布局,为表格计算工具栏和分页之外的剩余高度。 */ +.welfare-full-height-page .welfare-list-card > .el-card__body { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + box-sizing: border-box; + overflow: hidden; +} + +.welfare-full-height-page .welfare-full-height-table { + flex: 1 1 0; + min-height: 0; +} + +/* 数据超出可视区域时,仅允许 Element UI 表格内容区域纵向滚动。 */ +.welfare-full-height-page .welfare-full-height-table .el-table__body-wrapper { + overflow-y: auto; +} + +.welfare-full-height-page .el-pagination-container { + flex: 0 0 auto; +} diff --git a/src/main/resources/views/platform/zhgh/welfare/mine/index.html b/src/main/resources/views/platform/zhgh/welfare/mine/index.html index 59ab51b..8b33d9b 100644 --- a/src/main/resources/views/platform/zhgh/welfare/mine/index.html +++ b/src/main/resources/views/platform/zhgh/welfare/mine/index.html @@ -2,10 +2,15 @@ layout("/layouts/platform.html"){ #--> + +
diff --git a/src/main/resources/views/platform/zhgh/welfare/projectMange/index.html b/src/main/resources/views/platform/zhgh/welfare/projectMange/index.html index 006dc86..7f18c35 100644 --- a/src/main/resources/views/platform/zhgh/welfare/projectMange/index.html +++ b/src/main/resources/views/platform/zhgh/welfare/projectMange/index.html @@ -2,10 +2,97 @@ layout("/layouts/platform.html"){ #--> + +