diff --git a/ruoyi-site/ruoyi-h5/doc/需求.md b/ruoyi-site/ruoyi-h5/doc/需求.md index 7e364c102..f354d9843 100644 --- a/ruoyi-site/ruoyi-h5/doc/需求.md +++ b/ruoyi-site/ruoyi-h5/doc/需求.md @@ -138,7 +138,15 @@ ## 表单需求 帮我理一下这个表单需求,然后帮我合理设计一下表结构和技术选型,再实现表单提交接口和表单提交过的数据查询接口等等,有遗漏的记得帮我补上,在h5在线编辑中,可以新建个表单,表单可以自定义提交项,这种自定义表单,如何设计表结构,是用mongo还是mysql, -然后我看其他平台是这样提交表单内容的: +1、其他平台是这样提交表单内容的: {"templet_form_id":450055,"form_id":"461709","form_data":{"19fef684-b6ef-4bfb-8d68-603c99444ac5":"文本内容","6cbf9138-a20d-428c-b635-291246da3926":"13255556645","14154ccc-c2b3-4d27-8eb2-e3e422af294f":"1713322582323@qq.com"}} -表单的结构是保存在 H5ProjectContent 下的 Element(type=form时)->formItems 中的,formItems 的结构是如下: +2、表单的结构是保存在 H5ProjectContent 下的 Element(type=form时)->formItems 中的,formItems 的结构是如下: {"formItems": [ { "prop": "cf1d1fd1-9df9-44b4-ba73-3c553784aeab", "widget": "input", "verify": { "enable": false, "hint": "请输入文本内容" } }, { "prop": "56d54498-7d9d-4055-badd-09596fc9a43c", "widget": "input", "verify": { "enable": false, "hint": "请输入文本内容" } }, { "prop": "073f7b1f-afb9-4810-8896-d760197f5d02", "widget": "input", "verify": { "enable": true, "hint": "请输入正确的手机号码" } }, { "prop": "155ed883-93e5-4509-88db-ace73caac0d2", "widget": "input", "verify": { "enable": false, "hint": "请输入文本内容" } } ]} + +然后完善一下 H5ProjectContent 中的Element,目前有这几种类型,对应的内容有:image、 text、 svg、 group、 form、 map这几个,对应的内容结构如下: +image: { "uuid": "0.241109143257637336", "type": "image", "title": "4", "category": "", "opacity": 1, "padding": [ 0, 0, 0, 0 ], "width": 143, "height": 139, "left": 621, "top": -24, "transform": { "a": 1, "b": 0, "c": 0, "d": 1, "tx": 0, "ty": 0 }, "animations": [ { "type": "enter", "order": 0, "keyframes": { "opacity": [ 0, 1 ], "transform": [ "translate(0%, -82.73381294964028%) matrix(1, 0, 0, 1, 0, 0)", "translate(0, 0) scale(1) matrix(1, 0, 0, 1, 0, 0)" ] }, "delay": 0, "duration": 1000, "fill": "both", "name": "渐变和移动", "easing": " cubic-bezier(0.31, 0.21, 0.20, 1)", "customValues": { "direction": 0 }, "groupOrder": 1 } ], "boxShadow": null, "resize": 7, "dragable": true, "rotatable": true, "editable": true, "frozen": false, "hidden": false, "lock": false, "borderRadius": 0, "filter": { "contrast": 0, "sharpness": 0, "hueRotate": 0, "saturate": 0, "brightness": 0, "gaussianBlur": 0, "temperature": 0, "tint": 0 }, "watermarkEnable": false, "metaInfo": { "materials": [ { "id": 48218352, "type": "image" } ] }, "linkId": "", "groupable": true, "flex": null, "backgroundColor": null, "backgroundEffect": null, "border": null, "blendMode": "blendNormal", "maskInfo": { "enable": false, "showSelf": false, "imageUrl": "", "effectedResult": { "width": 0, "height": 0, "left": 0, "top": 0 } }, "suspension": { "enable": false, "top": null, "left": null, "right": null, "bottom": null }, "url": "https://st-gdx.dancf.com/odyssey/uxms/20211022-190216-5ef3.png", "imageUrl": "", "effectedResult": { "width": 0, "height": 0, "left": 0, "right": 0, "top": 0 }, "imageEffects": [], "aggregatedColors": [], "mainColor": null, "effectScale": 1, "imageTransform": { "a": 1, "b": 0, "c": 0, "d": 1, "tx": 0, "ty": 0 }, "filterInfo": null, "naturalWidth": 143, "naturalHeight": 139, "resourceType": "image", "naturalDuration": 0, "enableDragToMask": true, "loop": true, "canDownload": false, "version": "7.1.71", "events": [] } + text: { "uuid": "0.291946162773902226", "type": "text", "title": "地址导航", "category": "", "opacity": 1, "padding": [ 0, 0, 0, 0 ], "width": 186.541, "height": 40, "left": 79.72950000000003, "top": 3, "transform": { "a": 1, "b": 0, "c": 0, "d": 1, "tx": 0, "ty": 0 }, "animations": [], "boxShadow": null, "resize": 5, "dragable": false, "rotatable": false, "editable": true, "frozen": false, "hidden": false, "lock": false, "borderRadius": 0, "filter": { "contrast": 0, "sharpness": 0, "hueRotate": 0, "saturate": 0, "brightness": 0, "gaussianBlur": 0, "temperature": 0, "tint": 0 }, "watermarkEnable": false, "metaInfo": null, "linkId": "", "groupable": true, "flex": null, "backgroundColor": null, "backgroundEffect": null, "border": null, "blendMode": null, "maskInfo": { "enable": false, "showSelf": false, "imageUrl": "", "effectedResult": { "width": 0, "height": 0, "left": 0, "top": 0 } }, "suspension": { "enable": false, "top": null, "left": null, "right": null, "bottom": null }, "color": "#570002ff", "fontFamily": "SourceHanSansSC-Regular", "fontStyle": "normal", "fontWeight": 400, "fontSize": 33, "lineHeight": 1.2, "letterSpacing": 0, "textDecoration": "none", "writingMode": "horizontal-tb", "textAlign": "center", "verticalAlign": "top", "content": "地址导航", "contents": null, "effectScale": 1, "textShadow": null, "autoScale": true, "textEffects": [], "aggregatedColors": [ "#570002ff" ], "mainColor": null, "listStyle": "", "autoAdaptive": 1, "adjustsFontSize": false, "version": "6.1.71", "bindDataKey": "", "events": [] } +svg: { "uuid": "0.970693030997296142", "type": "svg", "title": "形状 2", "category": "", "opacity": 0.0784313725490196, "padding": [ 0, 0, 0, 0 ], "width": 3.000537872314453, "height": 668.0000002384186, "left": 26.999731063842773, "top": 0, "transform": { "a": 1, "b": 0, "c": 0, "d": 1, "tx": 0, "ty": 0 }, "animations": [], "boxShadow": null, "resize": 7, "dragable": false, "rotatable": false, "editable": true, "frozen": false, "hidden": false, "lock": false, "borderRadius": 0, "filter": { "contrast": 0, "sharpness": 0, "hueRotate": 0, "saturate": 0, "brightness": 0, "gaussianBlur": 0, "temperature": 0, "tint": 0 }, "watermarkEnable": false, "metaInfo": { "materials": [] }, "linkId": "", "groupable": true, "flex": null, "backgroundColor": null, "backgroundEffect": null, "border": null, "blendMode": null, "maskInfo": { "enable": false, "showSelf": false, "imageUrl": "", "effectedResult": { "width": 0, "height": 0, "left": 0, "top": 0 } }, "suspension": { "enable": false, "top": null, "left": null, "right": null, "bottom": null }, "url": "https://st-gdx.dancf.com/odyssey/uxms/20211022-190216-bb71.svg", "content": "", "colors": [ "#e9d196ff", "#fff1cfff", "#e9d196ff" ], "containerTransform": null, "version": "7.1.71", "events": [] } +group: { "uuid": "0.5223160352186436538", "type": "group", "title": "", "category": "", "opacity": 1, "padding": [ 0, 0, 0, 0 ], "width": 351.43835616438355, "height": 90.86839294433594, "left": 345.2801802125696, "top": 1209.7794354248047, "transform": { "a": 1, "b": 0, "c": 0, "d": 1, "tx": 0, "ty": 0 }, "animations": [ { "type": "enter", "order": 3, "keyframes": { "opacity": [ 0, 1 ], "transform": [ "translate(115.16096996485061%, 0%) matrix(1, 0, 0, 1, 0, 0)", "translate(0, 0) scale(1) matrix(1, 0, 0, 1, 0, 0)" ] }, "delay": 250, "duration": 1000, "fill": "both", "name": "渐变和移动", "easing": " cubic-bezier(0.31, 0.21, 0.20, 1)", "customValues": { "direction": 1 }, "groupOrder": 1 } ], "boxShadow": null, "resize": 1, "dragable": true, "rotatable": true, "editable": true, "frozen": false, "hidden": false, "lock": false, "borderRadius": 0, "filter": { "contrast": 0, "sharpness": 0, "hueRotate": 0, "saturate": 0, "brightness": 0, "gaussianBlur": 0, "temperature": 0, "tint": 0 }, "watermarkEnable": false, "metaInfo": null, "linkId": "", "groupable": true, "flex": null, "backgroundColor": null, "backgroundEffect": null, "border": null, "blendMode": null, "maskInfo": { "enable": false, "showSelf": false, "imageUrl": "", "effectedResult": { "width": 0, "height": 0, "left": 0, "top": 0 } }, "suspension": { "enable": false, "top": null, "left": null, "right": null, "bottom": null }, "elements": [ { "uuid": "0.4198633247682788452", "type": "text", "title": "中国·厦门", "category": "", "opacity": 1, "padding": [ 0, 0, 0, 0 ], "width": 252.629, "height": 43, "left": 98.80935616438364, "top": 47.86839294433594, "transform": { "a": 1, "b": 0, "c": 0, "d": 1, "tx": 0, "ty": 0 }, "animations": [], "boxShadow": null, "resize": 5, "dragable": false, "rotatable": false, "editable": true, "frozen": false, "hidden": false, "lock": false, "borderRadius": 0, "filter": { "contrast": 0, "sharpness": 0, "hueRotate": 0, "saturate": 0, "brightness": 0, "gaussianBlur": 0, "temperature": 0, "tint": 0 }, "watermarkEnable": false, "metaInfo": null, "linkId": "", "groupable": true, "flex": null, "backgroundColor": null, "backgroundEffect": null, "border": null, "blendMode": null, "maskInfo": { "enable": false, "showSelf": false, "imageUrl": "", "effectedResult": { "width": 0, "height": 0, "left": 0, "top": 0 } }, "suspension": { "enable": false, "top": null, "left": null, "right": null, "bottom": null }, "color": "#fff4d5ff", "fontFamily": "SourceHanSansSC-Regular", "fontStyle": "normal", "fontWeight": 400, "fontSize": 36, "lineHeight": 1.2, "letterSpacing": 0, "textDecoration": "none", "writingMode": "horizontal-tb", "textAlign": "right", "verticalAlign": "top", "content": "中国·厦门", "contents": null, "effectScale": 1, "textShadow": null, "autoScale": true, "textEffects": [], "aggregatedColors": [ "#fff4d5ff" ], "mainColor": null, "listStyle": "", "autoAdaptive": 1, "adjustsFontSize": false, "version": "6.1.71", "events": [], "bindDataKey": "" } ], "splitenable": true, "autoGrow": false, "version": "7.1.71", "events": [] } +form: {"uuid": "0.6779576535240048515", "type": "form", "title": "", "category": "", "opacity": 1, "padding": [0, 0, 0, 0], "width": 510.5, "height": 779, "left": 119.75000000000001, "top": 415.7632093933465, "transform": {"a": 1, "b": 0, "c": 0, "d": 1, "tx": 0, "ty": 0}, "animations": [{"type": "enter", "order": 3, "keyframes": {"opacity": [0, 1], "transform": ["scale(0.7) matrix(1, 0, 0, 1, 0, 0)", "translate(0, 0) scale(1) matrix(1, 0, 0, 1, 0, 0)"]}, "delay": 0, "duration": 1000, "fill": "both", "name": "渐变和缩放", "easing": "ease-in-out", "groupOrder": 3}], "boxShadow": null, "resize": 14, "dragable": true, "rotatable": true, "editable": true, "frozen": false, "hidden": false, "lock": false, "borderRadius": 0, "filter": {"contrast": 0, "sharpness": 0, "hueRotate": 0, "saturate": 0, "brightness": 0, "gaussianBlur": 0, "temperature": 0, "tint": 0}, "watermarkEnable": false, "metaInfo": null, "linkId": "", "groupable": true, "flex": null, "backgroundColor": null, "backgroundEffect": null, "border": null, "blendMode": null, "maskInfo": {"enable": false, "showSelf": false, "imageUrl": "", "effectedResult": {"width": 0, "height": 0, "left": 0, "top": 0}}, "suspension": {"enable": false, "top": null, "left": null, "right": null, "bottom": null}, "formData": {"cf1d1fd1-9df9-44b4-ba73-3c553784aeab": "", "073f7b1f-afb9-4810-8896-d760197f5d02": "", "56d54498-7d9d-4055-badd-09596fc9a43c": "", "155ed883-93e5-4509-88db-ace73caac0d2": ""}, "formOptions": {"id": "ee0c7225-cc60-4a16-9cf3-e2d313dd1309", "started_at": "", "finished_at": "", "auth": {"wechat": false}}, "formSchema": {"type": "object", "required": ["cf1d1fd1-9df9-44b4-ba73-3c553784aeab", "073f7b1f-afb9-4810-8896-d760197f5d02", "155ed883-93e5-4509-88db-ace73caac0d2"], "properties": {"cf1d1fd1-9df9-44b4-ba73-3c553784aeab": {"type": "string", "title": "姓名", "format": "string", "pattern": ".+", "description": ""}, "073f7b1f-afb9-4810-8896-d760197f5d02": {"type": "string", "title": "请输入手机号", "default": "", "format": "string", "pattern": "^([+\\d\\s/-]+)?([1]([3-9])[0-9]{9})$", "description": ""}, "56d54498-7d9d-4055-badd-09596fc9a43c": {"type": "string", "title": "人数", "format": "string", "pattern": ".+", "description": ""}, "155ed883-93e5-4509-88db-ace73caac0d2": {"type": "string", "title": "备注", "format": "string", "pattern": ".+", "description": ""}}}, "formTheme": {"colors": ["#ffe8a3ff", "#ffffffff", "#ffe8a3ff", "#ffe8a3ff", "#ffffff00"], "common": {"verticalPadding": 27}, "question": {"space": 20}, "image": {"column": 2, "width": 285, "height": 285}, "border": {"width": 1, "radius": 40}, "hintColors": ["#de415c", "#de415c", "#fdf7f8"], "text": {"submitText": "提交"}}, "formItems": [{"prop": "cf1d1fd1-9df9-44b4-ba73-3c553784aeab", "widget": "input", "verify": {"enable": false, "hint": "请输入文本内容"}}, {"prop": "56d54498-7d9d-4055-badd-09596fc9a43c", "widget": "input", "verify": {"enable": false, "hint": "请输入文本内容"}}, {"prop": "073f7b1f-afb9-4810-8896-d760197f5d02", "widget": "input", "verify": {"enable": true, "hint": "请输入正确的手机号码"}}, {"prop": "155ed883-93e5-4509-88db-ace73caac0d2", "widget": "input", "verify": {"enable": false, "hint": "请输入文本内容"}}], "fontFamily": "SourceHanSansSC-Regular", "fontStyle": "normal", "fontWeight": 400, "fontSize": 25, "textDecoration": "", "content": "-0123456789:交人入号名备姓年手择提数日月机注请输选", "contents": [{"color": "", "fontFamily": "SourceHanSansSC-Regular", "fontSize": 25, "fontStyle": "normal", "fontWeight": 400, "textDecoration": "", "content": "-0123456789:交人入号名备姓年手择提数日月机注请输选"}], "minWidth": 400, "maxWidth": 750, "minHeight": 509, "maxHeight": 5000, "version": "7.1.71", "events": [], "color": ""} +map: { "uuid": "0.548034526304074540", "type": "map", "title": "", "category": "", "opacity": 1, "padding": [ 0, 0, 0, 0 ], "width": 522.6666666666666, "height": 392, "left": 117, "top": 436, "transform": { "a": 1, "b": 0, "c": 0, "d": 1, "tx": 0, "ty": 0 }, "animations": [ { "type": "enter", "order": 4, "keyframes": { "opacity": [ 0, 1 ], "transform": [ "scale(0.7) matrix(1, 0, 0, 1, 0, 0)", "translate(0, 0) scale(1) matrix(1, 0, 0, 1, 0, 0)" ] }, "delay": 0, "duration": 1000, "fill": "both", "name": "渐变和缩放", "easing": "ease-in-out", "groupOrder": 0 } ], "boxShadow": null, "resize": 7, "dragable": true, "rotatable": true, "editable": true, "frozen": false, "hidden": false, "lock": false, "borderRadius": 0, "filter": { "contrast": 0, "sharpness": 0, "hueRotate": 0, "saturate": 0, "brightness": 0, "gaussianBlur": 0, "temperature": 0, "tint": 0 }, "watermarkEnable": false, "metaInfo": null, "linkId": "", "groupable": true, "flex": null, "backgroundColor": null, "backgroundEffect": null, "border": null, "blendMode": null, "maskInfo": { "enable": false, "showSelf": false, "imageUrl": "", "effectedResult": { "width": 0, "height": 0, "left": 0, "top": 0 } }, "suspension": { "enable": false, "top": null, "left": null, "right": null, "bottom": null }, "longitude": 118.186395, "latitude": 24.50876, "address": "厦门稿定科技", "zoom": 17, "borderWidth": [ 0, 0, 0, 0 ], "fontSize": 32, "borderRadiusX": 0, "borderRadiusY": 0, "borderColor": "#E0E5EA", "color": "#ffffffff", "version": "7.1.71", "imageDataUrl": "https://st-gdx.dancf.com/odyssey/uxms/20211026-111338-d7dd.png" } diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/config/JsonConfig.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/config/JsonConfig.java index 7d0e0aecb..925e316d4 100644 --- a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/config/JsonConfig.java +++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/config/JsonConfig.java @@ -22,11 +22,14 @@ import java.time.format.DateTimeFormatter; public class JsonConfig { @Bean public ObjectMapper mapperObject() { - JavaTimeModule module = new JavaTimeModule(); - module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); - module.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); - module.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss"))); - return new ObjectMapper().registerModule(module); + JavaTimeModule timeModule = new JavaTimeModule(); + timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + timeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + timeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss"))); + + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(timeModule); + return mapper; } -} +} \ No newline at end of file diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/controller/FormController.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/controller/FormController.java new file mode 100644 index 000000000..4153d4810 --- /dev/null +++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/controller/FormController.java @@ -0,0 +1,170 @@ +package com.luban.api.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.luban.api.entity.FormSubmission; +import com.luban.api.mongo.entity.FormData; +import com.luban.api.service.FormService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 表单控制器 + *

+ * + * @author your-name + */ +@RestController +@RequestMapping("/form") +@Tag(name = "表单管理", description = "表单提交与查询") +@Slf4j +public class FormController { + + @Autowired + private FormService formService; + + /** + * 提交表单数据 + * + * @param request 请求体包含表单数据 + * @return 是否提交成功 + */ + @Operation(summary = "提交表单数据") + @PostMapping("/submit") + public Boolean submitForm(@RequestBody SubmitFormRequest request, HttpServletRequest httpRequest) { + // 获取客户端IP地址 + String ipAddress = getClientIpAddress(httpRequest); + // 获取User-Agent + String userAgent = httpRequest.getHeader("User-Agent"); + + return formService.submitFormData( + request.getTemplateFormId(), + request.getFormId(), + request.getFormData(), + ipAddress, + userAgent + ); + } + + /** + * 根据模板表单ID分页查询表单提交记录 + * + * @param templateFormId 模板表单ID + * @param pageNum 页码 + * @param pageSize 每页大小 + * @return 表单提交记录分页数据 + */ + @Operation(summary = "根据模板表单ID分页查询表单提交记录") + @GetMapping("/page/template/{templateFormId}") + public IPage pageByTemplateFormId( + @PathVariable Long templateFormId, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + return formService.pageFormSubmissionsByTemplateFormId(templateFormId, pageNum, pageSize); + } + + /** + * 根据表单ID分页查询表单提交记录 + * + * @param formId 表单ID + * @param pageNum 页码 + * @param pageSize 每页大小 + * @return 表单提交记录分页数据 + */ + @Operation(summary = "根据表单ID分页查询表单提交记录") + @GetMapping("/page/form/{formId}") + public IPage pageByFormId( + @PathVariable String formId, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + return formService.pageFormSubmissionsByFormId(formId, pageNum, pageSize); + } + + /** + * 根据提交记录ID获取表单数据详情 + * + * @param submissionId 提交记录ID + * @return 表单数据详情 + */ + @Operation(summary = "根据提交记录ID获取表单数据详情") + @GetMapping("/detail/{submissionId}") + public FormData getFormDataDetail(@PathVariable Long submissionId) { + return formService.getFormDataDetail(submissionId); + } + + /** + * 根据模板表单ID获取所有表单数据 + * + * @param templateFormId 模板表单ID + * @return 表单数据列表 + */ + @Operation(summary = "根据模板表单ID获取所有表单数据") + @GetMapping("/list/template/{templateFormId}") + public List listByTemplateFormId(@PathVariable Long templateFormId) { + return formService.listFormDataByTemplateFormId(templateFormId); + } + + /** + * 根据表单ID获取所有表单数据 + * + * @param formId 表单ID + * @return 表单数据列表 + */ + @Operation(summary = "根据表单ID获取所有表单数据") + @GetMapping("/list/form/{formId}") + public List listByFormId(@PathVariable String formId) { + return formService.listFormDataByFormId(formId); + } + + /** + * 获取客户端IP地址 + * + * @param request HTTP请求 + * @return 客户端IP地址 + */ + private String getClientIpAddress(HttpServletRequest request) { + String ipAddress = request.getHeader("X-Forwarded-For"); + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("Proxy-Client-IP"); + } + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("WL-Proxy-Client-IP"); + } + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getRemoteAddr(); + } + return ipAddress; + } + + /** + * 提交表单请求类 + */ + @Setter + @Getter + public static class SubmitFormRequest { + /** + * 模板表单ID + */ + private Long templateFormId; + + /** + * 表单ID + */ + private String formId; + + /** + * 表单数据 + */ + private Map formData; + + } +} diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/dao/FormSubmissionDao.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/dao/FormSubmissionDao.java new file mode 100644 index 000000000..5b3a92093 --- /dev/null +++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/dao/FormSubmissionDao.java @@ -0,0 +1,15 @@ +package com.luban.api.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.luban.api.entity.FormSubmission; + +/** + *

+ * 表单提交记录 Mapper 接口 + *

+ * + * @author your-name + */ +public interface FormSubmissionDao extends BaseMapper { + +} \ No newline at end of file diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/entity/FormSubmission.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/entity/FormSubmission.java new file mode 100644 index 000000000..86a1aac63 --- /dev/null +++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/entity/FormSubmission.java @@ -0,0 +1,57 @@ +package com.luban.api.entity; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 表单提交记录实体类 + * + * @author your-name + */ +@Data +@EqualsAndHashCode +@Accessors(chain = true) +@TableName("h5_form_submission") +@Schema(title = "表单提交记录") +public class FormSubmission implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 模板表单ID(对应H5Project的ID) + */ + private Long templateFormId; + + /** + * 表单ID(实例ID) + */ + private String formId; + + /** + * 提交时间戳 + */ + private Long submitTime; + + /** + * 提交者IP地址 + */ + private String ipAddress; + + /** + * 用户代理信息 + */ + private String userAgent; +} diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/mongo/dao/FormDataRepository.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/mongo/dao/FormDataRepository.java new file mode 100644 index 000000000..aff80a748 --- /dev/null +++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/mongo/dao/FormDataRepository.java @@ -0,0 +1,30 @@ +package com.luban.api.mongo.dao; + +import com.luban.api.mongo.entity.FormData; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +/** + * 表单数据 Repository + * + * @author lizhw + */ +public interface FormDataRepository extends MongoRepository { + + /** + * 根据模板表单ID查询表单数据列表 + * + * @param templateFormId 模板表单ID + * @return 表单数据列表 + */ + List findByTemplateFormId(Long templateFormId); + + /** + * 根据表单ID查询表单数据列表 + * + * @param formId 表单ID + * @return 表单数据列表 + */ + List findByFormId(String formId); +} diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/mongo/entity/FormData.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/mongo/entity/FormData.java new file mode 100644 index 000000000..6d069952c --- /dev/null +++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/mongo/entity/FormData.java @@ -0,0 +1,58 @@ +package com.luban.api.mongo.entity; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Map; + +/** + * 表单数据实体类 (存储在MongoDB中) + * + * @author your-name + */ +@Data +@Document(collection = "h5_form_data") +public class FormData { + + /** + * 主键ID + */ + @Id + private Long id; + + /** + * 提交记录ID(对应MySQL中FormSubmission的ID) + */ + private Long submissionId; + + /** + * 模板表单ID(对应H5Project的ID) + */ + private Long templateFormId; + + /** + * 表单ID(实例ID) + */ + private String formId; + + /** + * 表单数据(key为formItem的prop,value为用户填写的内容) + */ + private Map formData; + + /** + * 提交时间戳 + */ + private Long submitTime; + + /** + * 提交者IP地址 + */ + private String ipAddress; + + /** + * 用户代理信息 + */ + private String userAgent; +} \ No newline at end of file diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/mongo/entity/H5ProjectContent.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/mongo/entity/H5ProjectContent.java index fe1f9b475..48967fb0e 100644 --- a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/mongo/entity/H5ProjectContent.java +++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/mongo/entity/H5ProjectContent.java @@ -1,6 +1,11 @@ package com.luban.api.mongo.entity; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @@ -159,11 +164,56 @@ public class H5ProjectContent { } /** - * 元素类 + * 元素类型枚举 + */ + @Getter + public enum ElementType { + IMAGE("image"), + TEXT("text"), + SVG("svg"), + GROUP("group"), + FORM("form"), + MAP("map"); + + private final String value; + + ElementType(String value) { + this.value = value; + } + + public static ElementType fromValue(String value) { + for (ElementType type : ElementType.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("Unknown element type: " + value); + } + } + + /** + * 元素基类 */ @Data + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type", + defaultImpl = Element.class + ) + @JsonSubTypes({ + @JsonSubTypes.Type(value = ImageElement.class, name = "image"), + @JsonSubTypes.Type(value = TextElement.class, name = "text"), + @JsonSubTypes.Type(value = SvgElement.class, name = "svg"), + @JsonSubTypes.Type(value = GroupElement.class, name = "group"), + @JsonSubTypes.Type(value = FormElement.class, name = "form"), + @JsonSubTypes.Type(value = MapElement.class, name = "map") + }) + @JsonIgnoreProperties(ignoreUnknown = true) public static class Element { - private String type; + private String uuid; + private ElementType type; + private String title; private String category; private Double opacity; private List padding; @@ -186,6 +236,24 @@ public class H5ProjectContent { private Map metaInfo; private String linkId; private Boolean groupable; + private Object flex; + private Object backgroundColor; + private Object backgroundEffect; + private Object border; + private Object blendMode; + private MaskInfo maskInfo; + private Suspension suspension; + private String version; + private List animations; + private List events; + } + + /** + * 图片元素类 + */ + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ImageElement extends Element { private String url; private String imageUrl; private Integer effectedImageWidth; @@ -200,19 +268,275 @@ public class H5ProjectContent { private FilterInfo filterInfo; private Integer naturalWidth; private Integer naturalHeight; - private String version; - private List animations; + private String resourceType; + private Integer naturalDuration; + private Boolean enableDragToMask; + private Boolean loop; + private Boolean canDownload; + + public ImageElement() { + this.setType(ElementType.IMAGE); + } + } + + /** + * 文本元素类 + */ + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class TextElement extends Element { + private String color; + private String fontFamily; + private String fontStyle; + private Integer fontWeight; + private Integer fontSize; + private Double lineHeight; + private Integer letterSpacing; + private Object textDecoration; + private String writingMode; + private String textAlign; + private String verticalAlign; private String content; private List contents; private Boolean autoScale; private List textEffects; private String listStyle; private Integer autoAdaptive; + private Boolean adjustsFontSize; + private Object textShadow; + private String bindDataKey; + + public TextElement() { + this.setType(ElementType.TEXT); + } + } + + /** + * SVG元素类 + */ + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SvgElement extends Element { + private String url; + private String content; + private List colors; + private Object containerTransform; + + public SvgElement() { + this.setType(ElementType.SVG); + } + } + + /** + * 组元素类 + */ + @Data + @EqualsAndHashCode(callSuper = true) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class GroupElement extends Element { + private List elements; + private Boolean splitenable; + private Boolean autoGrow; + + public GroupElement() { + this.setType(ElementType.GROUP); + } + } + + /** + * 表单元素类 + */ + @Data + @EqualsAndHashCode(callSuper = true) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class FormElement extends Element { + private Map formData; + private FormOptions formOptions; + private FormSchema formSchema; + private FormTheme formTheme; + private List formItems; + private String fontFamily; + private String fontStyle; + private Integer fontWeight; + private Integer fontSize; private Object textDecoration; - private String writingMode; - private String textAlign; - private String verticalAlign; - private List parents; + private String content; + private List contents; + private Integer minWidth; + private Integer maxWidth; + private Integer minHeight; + private Integer maxHeight; + + public FormElement() { + this.setType(ElementType.FORM); + } + } + + /** + * 地图元素类 + */ + @Data + @EqualsAndHashCode(callSuper = true) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class MapElement extends Element { + // Map类型的具体字段可以根据需要添加 + + public MapElement() { + this.setType(ElementType.MAP); + } + } + + /** + * 遮罩信息类 + */ + @Data + public static class MaskInfo { + private Boolean enable; + private Boolean showSelf; + private String imageUrl; + private EffectedResult effectedResult; + } + + /** + * 生效结果类 + */ + @Data + public static class EffectedResult { + private Integer width; + private Integer height; + private Integer left; + private Integer top; + private Integer right; + } + + /** + * 悬浮设置类 + */ + @Data + public static class Suspension { + private Boolean enable; + private Object top; + private Object left; + private Object right; + private Object bottom; + } + + /** + * 表单选项类 + */ + @Data + public static class FormOptions { + private String id; + private String started_at; + private String finished_at; + private Auth auth; + } + + /** + * 认证类 + */ + @Data + public static class Auth { + private Boolean wechat; + } + + /** + * 表单结构类 + */ + @Data + public static class FormSchema { + private String type; + private List required; + private Map properties; + } + + /** + * 属性类 + */ + @Data + public static class Property { + private String type; + private String title; + private String format; + private String pattern; + private String description; + private String defaultValue; + } + + /** + * 表单主题类 + */ + @Data + public static class FormTheme { + private List colors; + private Common common; + private Question question; + private Image image; + private Border border; + private List hintColors; + private Text text; + } + + /** + * 通用设置类 + */ + @Data + public static class Common { + private Integer verticalPadding; + } + + /** + * 问题设置类 + */ + @Data + public static class Question { + private Integer space; + } + + /** + * 图片设置类 + */ + @Data + public static class Image { + private Integer column; + private Integer width; + private Integer height; + } + + /** + * 边框设置类 + */ + @Data + public static class Border { + private Integer width; + private Integer radius; + } + + /** + * 文本设置类 + */ + @Data + public static class Text { + private String submitText; + } + + /** + * 表单项类 + */ + @Data + public static class FormItem { + private String prop; + private String widget; + private Verify verify; + } + + /** + * 验证类 + */ + @Data + public static class Verify { + private Boolean enable; + private String hint; } /** @@ -234,15 +558,11 @@ public class H5ProjectContent { private String type; private Integer order; private Map keyframes; - private List easing; - private List offset; private Integer delay; private Integer duration; private String fill; private String name; - private Object customValues; + private String easing; private Integer groupOrder; - private List excludeScale; - private String method; } -} \ No newline at end of file +} diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/service/FormService.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/service/FormService.java new file mode 100644 index 000000000..653a48822 --- /dev/null +++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/service/FormService.java @@ -0,0 +1,75 @@ +package com.luban.api.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import com.luban.api.entity.FormSubmission; +import com.luban.api.mongo.entity.FormData; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 表单服务类 + *

+ * + * @author lizhw + */ +public interface FormService extends IService { + + /** + * 提交表单数据 + * + * @param templateFormId 模板表单ID + * @param formId 表单ID + * @param formData 表单数据 + * @param ipAddress IP地址 + * @param userAgent 用户代理 + * @return 是否提交成功 + */ + boolean submitFormData(Long templateFormId, String formId, Map formData, String ipAddress, String userAgent); + + /** + * 根据模板表单ID分页查询表单提交记录 + * + * @param templateFormId 模板表单ID + * @param pageNum 页码 + * @param pageSize 每页大小 + * @return 表单提交记录分页数据 + */ + IPage pageFormSubmissionsByTemplateFormId(Long templateFormId, Integer pageNum, Integer pageSize); + + /** + * 根据表单ID分页查询表单提交记录 + * + * @param formId 表单ID + * @param pageNum 页码 + * @param pageSize 每页大小 + * @return 表单提交记录分页数据 + */ + IPage pageFormSubmissionsByFormId(String formId, Integer pageNum, Integer pageSize); + + /** + * 根据提交记录ID获取表单数据详情 + * + * @param submissionId 提交记录ID + * @return 表单数据详情 + */ + FormData getFormDataDetail(Long submissionId); + + /** + * 根据模板表单ID获取所有表单数据 + * + * @param templateFormId 模板表单ID + * @return 表单数据列表 + */ + List listFormDataByTemplateFormId(Long templateFormId); + + /** + * 根据表单ID获取所有表单数据 + * + * @param formId 表单ID + * @return 表单数据列表 + */ + List listFormDataByFormId(String formId); +} diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/service/impl/FormServiceImpl.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/service/impl/FormServiceImpl.java new file mode 100644 index 000000000..3f97ab838 --- /dev/null +++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/service/impl/FormServiceImpl.java @@ -0,0 +1,93 @@ +package com.luban.api.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.luban.api.dao.FormSubmissionDao; +import com.luban.api.entity.FormSubmission; +import com.luban.api.mongo.dao.FormDataRepository; +import com.luban.api.mongo.entity.FormData; +import com.luban.api.service.FormService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 表单服务实现类 + *

+ * + * @author lizhw + */ +@Service +public class FormServiceImpl extends ServiceImpl implements FormService { + + @Autowired + private FormDataRepository formDataRepository; + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean submitFormData(Long templateFormId, String formId, Map formData, String ipAddress, String userAgent) { + // 保存到MySQL + FormSubmission formSubmission = new FormSubmission(); + formSubmission.setTemplateFormId(templateFormId); + formSubmission.setFormId(formId); + long currentTime = System.currentTimeMillis() / 1000; + formSubmission.setSubmitTime(currentTime); + formSubmission.setIpAddress(ipAddress); + formSubmission.setUserAgent(userAgent); + this.save(formSubmission); + + // 保存到MongoDB + FormData formDataEntity = new FormData(); + formDataEntity.setSubmissionId(formSubmission.getId()); + formDataEntity.setTemplateFormId(templateFormId); + formDataEntity.setFormId(formId); + formDataEntity.setFormData(formData); + formDataEntity.setSubmitTime(currentTime); + formDataEntity.setIpAddress(ipAddress); + formDataEntity.setUserAgent(userAgent); + formDataRepository.save(formDataEntity); + + return true; + } + + @Override + public IPage pageFormSubmissionsByTemplateFormId(Long templateFormId, Integer pageNum, Integer pageSize) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FormSubmission::getTemplateFormId, templateFormId); + queryWrapper.orderByDesc(FormSubmission::getSubmitTime); + + IPage page = new Page<>(pageNum, pageSize); + return this.page(page, queryWrapper); + } + + @Override + public IPage pageFormSubmissionsByFormId(String formId, Integer pageNum, Integer pageSize) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FormSubmission::getFormId, formId); + queryWrapper.orderByDesc(FormSubmission::getSubmitTime); + + IPage page = new Page<>(pageNum, pageSize); + return this.page(page, queryWrapper); + } + + @Override + public FormData getFormDataDetail(Long submissionId) { + return formDataRepository.findById(submissionId).orElse(null); + } + + @Override + public List listFormDataByTemplateFormId(Long templateFormId) { + return formDataRepository.findByTemplateFormId(templateFormId); + } + + @Override + public List listFormDataByFormId(String formId) { + return formDataRepository.findByFormId(formId); + } +}