Compare commits
16
Commits
42e72aad99
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
360ce71f0b | ||
|
|
0a431bf86e | ||
|
|
9bebd3af82 | ||
|
|
e9866653a9 | ||
|
|
d8313d7822 | ||
|
|
a3c6c77d6b | ||
|
|
337bcd55bc | ||
|
|
3e99901d8e | ||
|
|
f7c381acbd | ||
|
|
c55449cd71 | ||
|
|
b05543d624 | ||
|
|
878b0de75f | ||
|
|
075448ab2b | ||
|
|
2af94299dd | ||
|
|
72cdae177e | ||
|
|
ae027378f7 |
@@ -0,0 +1,90 @@
|
|||||||
|
# 登山活动 1300 人并发报名压测
|
||||||
|
|
||||||
|
本目录用于本地或已明确授权的测试环境。JMeter 会真实调用登录和报名接口,并向报名表写入数据,禁止直接对生产环境执行。
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
- `prepare-brand-activity-users.ps1`:从当前项目数据库导出活动范围内、未禁用且尚未报名的人员。
|
||||||
|
- `brand-activity-mountain-signup-1300.jmx`:每线程独立登录,所有线程到达报名步骤后同时提交。
|
||||||
|
- `data/brand-activity-users.csv`:运行准备脚本后生成,不包含密码、手机号、身份证号等敏感数据。
|
||||||
|
|
||||||
|
当前登山活动参数:
|
||||||
|
|
||||||
|
- 活动 ID:`11619eee67574256ad8ccc8a16630023`
|
||||||
|
- 子活动 ID:`986bf4d48a5948e1810857483f9e5af2`
|
||||||
|
- 容量:1500 人
|
||||||
|
|
||||||
|
## 1. 准备人员数据
|
||||||
|
|
||||||
|
在项目根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\jmeter\prepare-brand-activity-users.ps1 -UserCount 1300 -PermissionMode AllScope
|
||||||
|
```
|
||||||
|
|
||||||
|
`AllScope` 会导出1300名活动范围人员。根据当前数据库,约290名人员没有报名菜单权限,其报名请求会被权限校验拒绝。
|
||||||
|
|
||||||
|
如果只测试具备权限的人员:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\jmeter\prepare-brand-activity-users.ps1 -UserCount 1010 -PermissionMode Authorized
|
||||||
|
```
|
||||||
|
|
||||||
|
当前数据库只有1010名“有报名权限且尚未报名”的人员,因此 `Authorized` 模式不能生成1300名不同人员。
|
||||||
|
|
||||||
|
## 2. 先进行10人试跑
|
||||||
|
|
||||||
|
启动本地项目,确认80端口和 Redis 6378 可访问,然后执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
jmeter -n -t .\jmeter\brand-activity-mountain-signup-1300.jmx `
|
||||||
|
-Jthreads=10 `
|
||||||
|
-JloginRampSeconds=10 `
|
||||||
|
-Jhost=127.0.0.1 `
|
||||||
|
-Jport=80 `
|
||||||
|
-JusersFile=jmeter/data/brand-activity-users.csv `
|
||||||
|
-l .\jmeter\result-smoke.jtl `
|
||||||
|
-j .\jmeter\jmeter-smoke.log
|
||||||
|
```
|
||||||
|
|
||||||
|
试跑会真实产生报名记录。再次运行前必须重新生成 CSV,以排除已经报名的人员。
|
||||||
|
|
||||||
|
## 3. 执行1300人并发
|
||||||
|
|
||||||
|
确认测试数据库已备份、应用和 Redis 正常、活动仍在报名时间内后执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
jmeter -n -t .\jmeter\brand-activity-mountain-signup-1300.jmx `
|
||||||
|
-Jthreads=1300 `
|
||||||
|
-JloginRampSeconds=60 `
|
||||||
|
-JsyncTimeoutMs=300000 `
|
||||||
|
-Jhost=127.0.0.1 `
|
||||||
|
-Jport=80 `
|
||||||
|
-JusersFile=jmeter/data/brand-activity-users.csv `
|
||||||
|
-l .\jmeter\result-1300.jtl `
|
||||||
|
-j .\jmeter\jmeter-1300.log
|
||||||
|
```
|
||||||
|
|
||||||
|
登录在60秒内逐步完成;所有仍在运行的线程会在“07-集合后并发提交登山活动报名”前等待,然后同时发送报名请求。
|
||||||
|
|
||||||
|
## 4. 生成结果报告
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
jmeter -g .\jmeter\result-1300.jtl -o .\jmeter\report-1300
|
||||||
|
```
|
||||||
|
|
||||||
|
重点查看:
|
||||||
|
|
||||||
|
- `05-人员账号登录`:独立账号登录是否成功。
|
||||||
|
- `06-报名资格预校验`:活动范围、时间、重复报名和名额检查。
|
||||||
|
- `07-集合后并发提交登山活动报名`:真正的并发报名结果。
|
||||||
|
|
||||||
|
报名结果只有返回 `code=0` 且 `msg=报名成功` 才计为成功,避免后端异常分支返回成功状态码但消息为“报名失败”时被误判。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 登录验证码由应用写入本机 Redis,JMX 仅在授权测试环境中从 Redis 读取对应验证码答案。
|
||||||
|
- 当前 `cas.enable=false`。现有登录实现只在 CAS 模式开启时拒绝密码不匹配,因此 JMX 使用测试占位密码;该行为不应复制到生产环境。
|
||||||
|
- Controller 使用单 JVM `ReentrantLock`,单节点下报名请求会排队;多节点下该锁无法跨节点保证一致性。
|
||||||
|
- JMeter 1300线程会消耗较多内存。若出现堆内存不足,可在启动前提高 `HEAP`,例如 `-Xms2g -Xmx4g`。
|
||||||
|
- 不提供自动删除报名数据的脚本,避免误删真实报名记录。
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
|
||||||
|
<hashTree>
|
||||||
|
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="品牌活动-登山活动1300人并发报名" enabled="true">
|
||||||
|
<stringProp name="TestPlan.comments">每个线程使用独立用户和 Cookie 登录,登录完成后在报名接口前集合并发。仅限本地或经授权的测试环境。</stringProp>
|
||||||
|
<boolProp name="TestPlan.functional_mode">false</boolProp>
|
||||||
|
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
|
||||||
|
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
|
||||||
|
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="业务参数" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="activityId" elementType="Argument">
|
||||||
|
<stringProp name="Argument.name">activityId</stringProp>
|
||||||
|
<stringProp name="Argument.value">${__P(activityId,11619eee67574256ad8ccc8a16630023)}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="courseId" elementType="Argument">
|
||||||
|
<stringProp name="Argument.name">courseId</stringProp>
|
||||||
|
<stringProp name="Argument.value">${__P(courseId,986bf4d48a5948e1810857483f9e5af2)}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="redisHost" elementType="Argument">
|
||||||
|
<stringProp name="Argument.name">redisHost</stringProp>
|
||||||
|
<stringProp name="Argument.value">${__P(redisHost,127.0.0.1)}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="redisPort" elementType="Argument">
|
||||||
|
<stringProp name="Argument.name">redisPort</stringProp>
|
||||||
|
<stringProp name="Argument.value">${__P(redisPort,6378)}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="testPassword" elementType="Argument">
|
||||||
|
<stringProp name="Argument.name">testPassword</stringProp>
|
||||||
|
<stringProp name="Argument.value">${__P(testPassword,LoadTestOnly@123)}</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
</TestPlan>
|
||||||
|
<hashTree>
|
||||||
|
<CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="读取数据库人员CSV" enabled="true">
|
||||||
|
<stringProp name="delimiter">,</stringProp>
|
||||||
|
<stringProp name="fileEncoding">UTF-8</stringProp>
|
||||||
|
<stringProp name="filename">${__P(usersFile,jmeter/data/brand-activity-users.csv)}</stringProp>
|
||||||
|
<boolProp name="ignoreFirstLine">false</boolProp>
|
||||||
|
<boolProp name="quotedData">true</boolProp>
|
||||||
|
<boolProp name="recycle">false</boolProp>
|
||||||
|
<stringProp name="shareMode">shareMode.all</stringProp>
|
||||||
|
<boolProp name="stopThread">true</boolProp>
|
||||||
|
<stringProp name="variableNames"></stringProp>
|
||||||
|
</CSVDataSet>
|
||||||
|
<hashTree/>
|
||||||
|
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="独立用户登录并并发报名" enabled="true">
|
||||||
|
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
|
||||||
|
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
|
||||||
|
<boolProp name="LoopController.continue_forever">false</boolProp>
|
||||||
|
<stringProp name="LoopController.loops">1</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="ThreadGroup.num_threads">${__P(threads,10)}</stringProp>
|
||||||
|
<stringProp name="ThreadGroup.ramp_time">${__P(loginRampSeconds,30)}</stringProp>
|
||||||
|
<boolProp name="ThreadGroup.scheduler">false</boolProp>
|
||||||
|
<stringProp name="ThreadGroup.duration"></stringProp>
|
||||||
|
<stringProp name="ThreadGroup.delay"></stringProp>
|
||||||
|
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
|
||||||
|
</ThreadGroup>
|
||||||
|
<hashTree>
|
||||||
|
<ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP默认配置" enabled="true">
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义变量" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments"/>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="HTTPSampler.domain">${__P(host,127.0.0.1)}</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">${__P(port,80)}</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">${__P(protocol,http)}</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.connect_timeout">${__P(connectTimeout,10000)}</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.response_timeout">${__P(responseTimeout,120000)}</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||||
|
</ConfigTestElement>
|
||||||
|
<hashTree/>
|
||||||
|
<CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="每线程独立Cookie" enabled="true">
|
||||||
|
<collectionProp name="CookieManager.cookies"/>
|
||||||
|
<boolProp name="CookieManager.clearEachIteration">true</boolProp>
|
||||||
|
<boolProp name="CookieManager.controlledByThreadGroup">false</boolProp>
|
||||||
|
</CookieManager>
|
||||||
|
<hashTree/>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="请求头" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="Content-Type" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Content-Type</stringProp>
|
||||||
|
<stringProp name="Header.value">application/x-www-form-urlencoded;charset=UTF-8</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="Accept" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Accept</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="01-获取图形验证码" enabled="true">
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="请求参数" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments"/>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/platform/login/captcha</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="提取验证码Key" enabled="true">
|
||||||
|
<stringProp name="JSONPostProcessor.referenceNames">captchaKey</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.jsonPathExprs">$.data.key</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.match_numbers">1</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND</stringProp>
|
||||||
|
</JSONPostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="02-从本地Redis读取压测验证码" enabled="true">
|
||||||
|
<stringProp name="cacheKey">true</stringProp>
|
||||||
|
<stringProp name="filename"></stringProp>
|
||||||
|
<stringProp name="parameters"></stringProp>
|
||||||
|
<stringProp name="scriptLanguage">groovy</stringProp>
|
||||||
|
<stringProp name="script"><![CDATA[
|
||||||
|
def captchaKey = vars.get('captchaKey')
|
||||||
|
if (!captchaKey || captchaKey == 'NOT_FOUND') {
|
||||||
|
throw new IllegalStateException('获取验证码Key失败')
|
||||||
|
}
|
||||||
|
def redisKey = 'budwk5mini:ucenter:captcha:' + captchaKey
|
||||||
|
def socket = new Socket(vars.get('redisHost'), vars.get('redisPort') as int)
|
||||||
|
socket.soTimeout = 3000
|
||||||
|
try {
|
||||||
|
def keyBytes = redisKey.getBytes('UTF-8')
|
||||||
|
def command = '*2\r\n$3\r\nGET\r\n$' + keyBytes.length + '\r\n' + redisKey + '\r\n'
|
||||||
|
socket.outputStream.write(command.getBytes('UTF-8'))
|
||||||
|
socket.outputStream.flush()
|
||||||
|
def reader = new BufferedReader(new InputStreamReader(socket.inputStream, 'UTF-8'))
|
||||||
|
def header = reader.readLine()
|
||||||
|
if (!header || !header.startsWith('$') || header == '$-1') {
|
||||||
|
throw new IllegalStateException('Redis中未找到验证码:' + redisKey)
|
||||||
|
}
|
||||||
|
def captchaCode = reader.readLine()
|
||||||
|
if (!captchaCode) {
|
||||||
|
throw new IllegalStateException('验证码内容为空')
|
||||||
|
}
|
||||||
|
vars.put('captchaCode', captchaCode)
|
||||||
|
} finally {
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
]]></stringProp>
|
||||||
|
</JSR223Sampler>
|
||||||
|
<hashTree/>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="03-获取登录RSA公钥" enabled="true">
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="请求参数" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments"/>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/platform/login/publicKey</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="提取公钥和KeyId" enabled="true">
|
||||||
|
<stringProp name="JSONPostProcessor.referenceNames">publicKey;keyId</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.jsonPathExprs">$.data.publicKey;$.data.keyId</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.match_numbers">1;1</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND;NOT_FOUND</stringProp>
|
||||||
|
</JSONPostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="04-RSA加密测试密码" enabled="true">
|
||||||
|
<stringProp name="cacheKey">true</stringProp>
|
||||||
|
<stringProp name="filename"></stringProp>
|
||||||
|
<stringProp name="parameters"></stringProp>
|
||||||
|
<stringProp name="scriptLanguage">groovy</stringProp>
|
||||||
|
<stringProp name="script"><![CDATA[
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
def publicKeyText = vars.get('publicKey')
|
||||||
|
if (!publicKeyText || publicKeyText == 'NOT_FOUND') {
|
||||||
|
throw new IllegalStateException('获取RSA公钥失败')
|
||||||
|
}
|
||||||
|
def keyBytes = Base64.decoder.decode(publicKeyText)
|
||||||
|
def publicKey = KeyFactory.getInstance('RSA').generatePublic(new X509EncodedKeySpec(keyBytes))
|
||||||
|
def cipher = Cipher.getInstance('RSA/ECB/PKCS1Padding')
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||||
|
def encrypted = cipher.doFinal(vars.get('testPassword').getBytes('UTF-8'))
|
||||||
|
vars.put('encryptedPassword', Base64.encoder.encodeToString(encrypted))
|
||||||
|
]]></stringProp>
|
||||||
|
</JSR223Sampler>
|
||||||
|
<hashTree/>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="05-人员账号登录" enabled="true">
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="登录参数" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="username" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">username</stringProp><stringProp name="Argument.value">${loginname}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="password" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">password</stringProp><stringProp name="Argument.value">${encryptedPassword}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="platformKey" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">platformKey</stringProp><stringProp name="Argument.value">${captchaKey}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="platformCaptcha" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">platformCaptcha</stringProp><stringProp name="Argument.value">${captchaCode}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="keyId" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">keyId</stringProp><stringProp name="Argument.value">${keyId}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/platform/login/doLogin</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="断言登录成功" enabled="true">
|
||||||
|
<stringProp name="scriptLanguage">groovy</stringProp>
|
||||||
|
<stringProp name="script"><![CDATA[
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
try {
|
||||||
|
def json = new JsonSlurper().parseText(prev.getResponseDataAsString())
|
||||||
|
if (json.code != 0) {
|
||||||
|
prev.setSuccessful(false)
|
||||||
|
prev.setResponseMessage('登录失败:' + json.msg)
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
prev.setSuccessful(false)
|
||||||
|
prev.setResponseMessage('登录响应不是有效JSON:' + e.message)
|
||||||
|
}
|
||||||
|
]]></stringProp>
|
||||||
|
</JSR223PostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="06-报名资格预校验" enabled="true">
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="校验参数" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="courseId" elementType="HTTPArgument"><boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">courseId</stringProp><stringProp name="Argument.value">${courseId}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp></elementProp>
|
||||||
|
<elementProp name="currentFamilyNumber" elementType="HTTPArgument"><boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">currentFamilyNumber</stringProp><stringProp name="Argument.value">0</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp></elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/platform/trainSignUp/apply/validateSignUp</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree/>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="07-集合后并发提交登山活动报名" enabled="true">
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="报名参数" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="activityId" elementType="HTTPArgument"><boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">activityId</stringProp><stringProp name="Argument.value">${activityId}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp></elementProp>
|
||||||
|
<elementProp name="courseId" elementType="HTTPArgument"><boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">courseId</stringProp><stringProp name="Argument.value">${courseId}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp></elementProp>
|
||||||
|
<elementProp name="mobileColumnsValue" elementType="HTTPArgument"><boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">mobileColumnsValue</stringProp><stringProp name="Argument.value">[]</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp></elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/platform/trainSignUp/apply/doSignUp</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<SyncTimer guiclass="TestBeanGUI" testclass="SyncTimer" testname="等待本线程组全部人员后同时报名" enabled="true">
|
||||||
|
<intProp name="groupSize">0</intProp>
|
||||||
|
<stringProp name="timeoutInMs">${__P(syncTimeoutMs,180000)}</stringProp>
|
||||||
|
</SyncTimer>
|
||||||
|
<hashTree/>
|
||||||
|
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="断言报名成功" enabled="true">
|
||||||
|
<stringProp name="scriptLanguage">groovy</stringProp>
|
||||||
|
<stringProp name="script"><![CDATA[
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
try {
|
||||||
|
def json = new JsonSlurper().parseText(prev.getResponseDataAsString())
|
||||||
|
if (json.code != 0 || json.msg != '报名成功') {
|
||||||
|
prev.setSuccessful(false)
|
||||||
|
prev.setResponseMessage('报名未成功;expectedAuthorized=' + vars.get('expectedAuthorized') + ';返回:' + json.msg)
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
prev.setSuccessful(false)
|
||||||
|
prev.setResponseMessage('报名响应不是有效JSON;expectedAuthorized=' + vars.get('expectedAuthorized') + ';' + e.message)
|
||||||
|
}
|
||||||
|
]]></stringProp>
|
||||||
|
</JSR223PostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
</hashTree>
|
||||||
|
</hashTree>
|
||||||
|
</hashTree>
|
||||||
|
</jmeterTestPlan>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
param(
|
||||||
|
[ValidateRange(1, 1300)]
|
||||||
|
[int]$UserCount = 1300,
|
||||||
|
|
||||||
|
[ValidateSet("AllScope", "Authorized")]
|
||||||
|
[string]$PermissionMode = "AllScope",
|
||||||
|
|
||||||
|
[ValidatePattern("^[0-9a-fA-F]{32}$")]
|
||||||
|
[string]$ActivityId = "11619eee67574256ad8ccc8a16630023",
|
||||||
|
|
||||||
|
[string]$OutputFile = (Join-Path $PSScriptRoot "data\brand-activity-users.csv"),
|
||||||
|
|
||||||
|
[string]$ApplicationConfig = (Join-Path $PSScriptRoot "..\src\main\resources\application.yaml")
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Get-YamlScalar {
|
||||||
|
param(
|
||||||
|
[string[]]$Lines,
|
||||||
|
[string]$Name
|
||||||
|
)
|
||||||
|
|
||||||
|
$matchedLine = $Lines | Where-Object { $_ -match ("^\s*" + [regex]::Escape($Name) + "\s*:\s*(.+?)\s*$") } | Select-Object -First 1
|
||||||
|
if (-not $matchedLine) {
|
||||||
|
throw "未在 application.yaml 中找到配置项:$Name"
|
||||||
|
}
|
||||||
|
return ([regex]::Match($matchedLine, "^\s*[^:]+:\s*(.+?)\s*$")).Groups[1].Value.Trim('"', "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $ApplicationConfig)) {
|
||||||
|
throw "找不到应用配置文件:$ApplicationConfig"
|
||||||
|
}
|
||||||
|
|
||||||
|
$mysqlCommand = Get-Command mysql -ErrorAction SilentlyContinue
|
||||||
|
if (-not $mysqlCommand) {
|
||||||
|
throw "未找到 mysql 命令,请先把 MySQL 客户端 bin 目录加入 PATH。"
|
||||||
|
}
|
||||||
|
|
||||||
|
$configLines = Get-Content -LiteralPath $ApplicationConfig -Encoding UTF8
|
||||||
|
$jdbcUrl = Get-YamlScalar -Lines $configLines -Name "url"
|
||||||
|
$dbUser = Get-YamlScalar -Lines $configLines -Name "username"
|
||||||
|
$dbPassword = Get-YamlScalar -Lines $configLines -Name "password"
|
||||||
|
|
||||||
|
$jdbcMatch = [regex]::Match($jdbcUrl, "jdbc:mysql://(?<host>[^:/?]+)(:(?<port>\d+))?/(?<database>[^?]+)")
|
||||||
|
if (-not $jdbcMatch.Success) {
|
||||||
|
throw "无法解析 JDBC 地址:$jdbcUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbHost = $jdbcMatch.Groups["host"].Value
|
||||||
|
$dbPort = if ($jdbcMatch.Groups["port"].Success) { $jdbcMatch.Groups["port"].Value } else { "3306" }
|
||||||
|
$database = $jdbcMatch.Groups["database"].Value
|
||||||
|
|
||||||
|
# 仅导出活动范围内、账号未禁用且尚未报名的人员;不读取密码、身份证号、手机号等敏感字段。
|
||||||
|
$permissionCondition = if ($PermissionMode -eq "Authorized") {
|
||||||
|
@"
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_user_role ur
|
||||||
|
JOIN sys_role_menu rm ON rm.roleId = ur.roleId
|
||||||
|
JOIN sys_menu m ON m.id = rm.menuId
|
||||||
|
WHERE ur.userId = u.id
|
||||||
|
AND m.permission IN ('trainSignUp.apply', 'h5.trainSignUp.apply')
|
||||||
|
)
|
||||||
|
"@
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = @"
|
||||||
|
SELECT
|
||||||
|
u.loginname,
|
||||||
|
CASE WHEN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_user_role ur
|
||||||
|
JOIN sys_role_menu rm ON rm.roleId = ur.roleId
|
||||||
|
JOIN sys_menu m ON m.id = rm.menuId
|
||||||
|
WHERE ur.userId = u.id
|
||||||
|
AND m.permission IN ('trainSignUp.apply', 'h5.trainSignUp.apply')
|
||||||
|
) THEN 1 ELSE 0 END AS expectedAuthorized
|
||||||
|
FROM train_sign_up_activity a
|
||||||
|
JOIN activity_user_scope aus ON aus.groupId = a.activityGroupId
|
||||||
|
JOIN sys_user u ON u.id = aus.userId
|
||||||
|
LEFT JOIN train_sign_up_user su ON su.activityId = a.id AND su.userId = u.id
|
||||||
|
WHERE a.id = '$ActivityId'
|
||||||
|
AND u.disabled = 0
|
||||||
|
AND u.loginname IS NOT NULL
|
||||||
|
AND u.loginname <> ''
|
||||||
|
AND su.id IS NULL
|
||||||
|
$permissionCondition
|
||||||
|
GROUP BY u.id, u.loginname
|
||||||
|
ORDER BY expectedAuthorized DESC, u.loginname
|
||||||
|
LIMIT $UserCount;
|
||||||
|
"@
|
||||||
|
|
||||||
|
$previousMysqlPassword = $env:MYSQL_PWD
|
||||||
|
try {
|
||||||
|
$env:MYSQL_PWD = $dbPassword
|
||||||
|
$rows = @(& $mysqlCommand.Source --batch --raw --skip-column-names -h $dbHost -P $dbPort -u $dbUser $database -e $query)
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "查询压测人员失败,mysql 退出码:$LASTEXITCODE"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$env:MYSQL_PWD = $previousMysqlPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = @($rows | Where-Object { $_ -and $_.Trim() } | ForEach-Object {
|
||||||
|
$columns = $_ -split "`t", 2
|
||||||
|
[pscustomobject]@{
|
||||||
|
loginname = $columns[0]
|
||||||
|
expectedAuthorized = $columns[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if ($users.Count -lt $UserCount) {
|
||||||
|
throw "符合条件的人员只有 $($users.Count) 人,少于要求的 $UserCount 人。PermissionMode=$PermissionMode"
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputDirectory = Split-Path -Parent $OutputFile
|
||||||
|
if (-not (Test-Path -LiteralPath $outputDirectory)) {
|
||||||
|
New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$users | Export-Csv -LiteralPath $OutputFile -NoTypeInformation -Encoding UTF8 -UseQuotes Always
|
||||||
|
|
||||||
|
$authorizedCount = @($users | Where-Object { $_.expectedAuthorized -eq "1" }).Count
|
||||||
|
$unauthorizedCount = $users.Count - $authorizedCount
|
||||||
|
Write-Host "已生成 UTF-8 用户文件:$OutputFile"
|
||||||
|
Write-Host "总人数:$($users.Count),具备报名权限:$authorizedCount,无报名权限:$unauthorizedCount"
|
||||||
|
if ($unauthorizedCount -gt 0) {
|
||||||
|
Write-Warning "无报名权限的线程仍会访问报名接口,但会被权限校验拒绝。"
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@ public class RedisConstant {
|
|||||||
//企业微信token
|
//企业微信token
|
||||||
public final static String REDIS_KEY_WX_WORK_TOKEN = PLATFORM_REDIS_PREFIX + "wxwork:token:";
|
public final static String REDIS_KEY_WX_WORK_TOKEN = PLATFORM_REDIS_PREFIX + "wxwork:token:";
|
||||||
|
|
||||||
|
// 学校消息中心钉钉提醒 Token;按 appId、compId 隔离,刷新锁与 Token 使用同一哈希槽。
|
||||||
|
public static final String REDIS_KEY_DING_MESSAGE_TOKEN = PLATFORM_REDIS_PREFIX + "ding:message:token:";
|
||||||
|
|
||||||
//健步走小程序TOKEN
|
//健步走小程序TOKEN
|
||||||
public final static String REDIS_KEY_WE_APP_ACCESS_TOKEN = PLATFORM_REDIS_PREFIX + "weapp:token:";
|
public final static String REDIS_KEY_WE_APP_ACCESS_TOKEN = PLATFORM_REDIS_PREFIX + "weapp:token:";
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ RoleConstant {
|
|||||||
SCHOOL_UNION_ARTICLE_FGH_ADMIN("校工会分工会新闻审批管理员"),
|
SCHOOL_UNION_ARTICLE_FGH_ADMIN("校工会分工会新闻审批管理员"),
|
||||||
SCHOOL_UNION_ARTICLE_ADMIN("校工会新闻审批管理员"),
|
SCHOOL_UNION_ARTICLE_ADMIN("校工会新闻审批管理员"),
|
||||||
SCHOOL_OUTLAY_ADMIN("校工会经费管理员"),
|
SCHOOL_OUTLAY_ADMIN("校工会经费管理员"),
|
||||||
|
TOUR_APPLICANT("疗休养领办人"),
|
||||||
|
|
||||||
BRANCH_UNION_ADMIN("分工会管理员"),
|
BRANCH_UNION_ADMIN("分工会管理员"),
|
||||||
BRANCH_UNION_CHAIRMAN("分工会主席"),
|
BRANCH_UNION_CHAIRMAN("分工会主席"),
|
||||||
|
|||||||
@@ -136,4 +136,7 @@ public interface SmsService {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
void recallMsg(String msgId);
|
void recallMsg(String msgId);
|
||||||
|
|
||||||
|
/** 后台群发卡片:工号列表、标题、正文、PC/H5 跳转地址及提交人的工号;无返回值。 */
|
||||||
|
void massSendTextCard(List<String> loginNames, String title, String content, String pcUrl, String mobileUrl, String fromUser);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
package com.budwk.app.base.sms.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.http.HttpRequest;
|
||||||
|
import cn.hutool.http.HttpResponse;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.budwk.app.base.constant.RedisConstant;
|
||||||
|
import com.budwk.app.base.sms.SmsService;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.web.commons.base.Globals;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
|
import org.nutz.integration.jedis.RedisService;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ClassName SmsServiceImpl
|
||||||
|
* @Author JyuHsin
|
||||||
|
* @Date 2025/11/26 19:32
|
||||||
|
* @Version 1.0
|
||||||
|
* 通过学校消息中心发送钉钉提醒,正文填充钉钉模板变量 #{content}。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@IocBean
|
||||||
|
public class SmsServiceImpl implements SmsService {
|
||||||
|
|
||||||
|
private static final String BASE_URL = "https://ua.zjipc.cn/messageng/openApi";
|
||||||
|
// 由信息中心提供,templateId 必须对应包含 #{content} 的钉钉模板。
|
||||||
|
private static final String APP_ID = "xy6t2ht3qkkh6jdx9cyhwmg4bhgldjn3";
|
||||||
|
private static final String APP_SECRET = "T_AtDFHS0KHVCvpZK5zpx6bnRuIDNcwu4BgFLOZxXi0w45HrTIto1q_UYmX8lnjf";
|
||||||
|
private static final String COMP_ID = "30c2b301a1759c0ebcb4550635066ac0";
|
||||||
|
private static final String TEMPLATE_ID = "DING_083";
|
||||||
|
// 异步任务无登录上下文,按文档示例使用系统发送者。
|
||||||
|
private static final int REQUEST_TIMEOUT = 5000;
|
||||||
|
// 锁覆盖认证超时窗口,进程退出后自动释放;写缓存时再次校验锁归属。
|
||||||
|
private static final int TOKEN_LOCK_SECONDS = 30;
|
||||||
|
private static final String DELETE_IF_MATCH = "if redis.call('get', KEYS[1]) == ARGV[1] then "
|
||||||
|
+ "return redis.call('del', KEYS[1]) else return 0 end";
|
||||||
|
private static final String CACHE_IF_OWNER = "if redis.call('get', KEYS[1]) == ARGV[1] then "
|
||||||
|
+ "redis.call('setex', KEYS[2], ARGV[2], ARGV[3]); return 1 else return 0 end";
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private RedisService redisService;
|
||||||
|
|
||||||
|
/** 按工号单发正文,使用默认标题和首页链接,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void sendSingleMessage(String loginName, String content) {
|
||||||
|
this.sendSingleMessage(loginName, "智慧工会", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按工号单发指定标题与正文,无返回值,链接默认使用首页。 */
|
||||||
|
@Override
|
||||||
|
public void sendSingleMessage(String loginName, String title, String content) {
|
||||||
|
this.massSendMessage(Collections.singletonList(loginName), title, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按工号发送正文与 PC/H5 业务链接,使用默认标题,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void sendSingleTextCard(String loginName, String content, String pcUrl, String mobileUrl) {
|
||||||
|
this.sendSingleTextCard(loginName, "智慧工会", content, pcUrl, mobileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按工号发送指定标题、正文与 PC/H5 业务链接,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void sendSingleTextCard(String loginName, String title, String content, String pcUrl, String mobileUrl) {
|
||||||
|
this.massSendTextCard(Collections.singletonList(loginName), title, content, pcUrl, mobileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 兼容原图文单发参数,使用默认标题,图片限制由群发入口统一处理,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void sendSinglePicTextCard(String loginName, String content, String pcUrl, String mobileUrl, String picUrl) {
|
||||||
|
this.sendSinglePicTextCard(loginName, "智慧工会", content, pcUrl, mobileUrl, picUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按工号接收指定标题的原图文参数,仅发送正文与业务链接,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void sendSinglePicTextCard(String loginName, String title, String content, String pcUrl, String mobileUrl, String picUrl) {
|
||||||
|
this.massSendPicTextCard(Collections.singletonList(loginName), title, content, pcUrl, mobileUrl, picUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向工号列表发送相同正文,默认标题和首页链接,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void massSendMessage(List<String> loginNames, String content) {
|
||||||
|
this.massSendMessage(loginNames, "智慧工会", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向工号列表发送指定标题和正文,默认使用首页链接,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void massSendMessage(List<String> loginNames, String title, String content) {
|
||||||
|
// 普通提醒也必须携带链接,未提供业务链接时使用系统首页。
|
||||||
|
this.doSend(loginNames, title, content, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向工号列表发送正文与 PC/H5 链接,使用默认标题,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void massSendTextCard(List<String> loginNames, String content, String pcUrl, String mobileUrl) {
|
||||||
|
this.massSendTextCard(loginNames, "智慧工会", content, pcUrl, mobileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向工号列表发送指定标题的卡片提醒,优先 H5 链接,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void massSendTextCard(List<String> loginNames, String title, String content, String pcUrl, String mobileUrl) {
|
||||||
|
this.doSend(loginNames, title, content, pcUrl, mobileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后台发送使用提交时取得的工号,不依赖工作线程的登录上下文;参数及返回约定见接口。 */
|
||||||
|
@Override
|
||||||
|
public void massSendTextCard(List<String> loginNames, String title, String content, String pcUrl, String mobileUrl, String fromUser) {
|
||||||
|
this.doSend(loginNames, title, content, pcUrl, mobileUrl, fromUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 兼容原图文群发参数,使用默认标题,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void massSendPicTextCard(List<String> loginNames, String content, String pcUrl, String mobileUrl, String picUrl) {
|
||||||
|
this.massSendPicTextCard(loginNames, "智慧工会", content, pcUrl, mobileUrl, picUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接收工号列表、标题、正文、PC/H5 链接和图片 URL;图片不发送,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void massSendPicTextCard(List<String> loginNames, String title, String content, String pcUrl, String mobileUrl, String picUrl) {
|
||||||
|
// 文档无图片 URL 参数,保留调用入口但按普通卡片发送,不能将图片误填为附件 ID。
|
||||||
|
if (StrUtil.isNotBlank(picUrl)) {
|
||||||
|
log.warn("钉钉消息中心暂不支持图片 URL,本次仅发送正文与链接");
|
||||||
|
}
|
||||||
|
this.doSend(loginNames, title, content, pcUrl, mobileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接收待撤回消息 ID;当前文档无撤回能力,仅记录未执行,无返回值。 */
|
||||||
|
@Override
|
||||||
|
public void recallMsg(String msgId) {
|
||||||
|
// 文档未提供撤回协议,保留接口兼容调用方,不再请求旧企业微信平台。
|
||||||
|
log.warn("钉钉消息中心文档未提供撤回接口,未执行撤回");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收工号列表、标题、正文和 PC/H5 链接;按文档逐人发送,无返回值。
|
||||||
|
* 正文映射为模板变量 content,单人失败记录后继续其余接收人,沿用原服务行为。
|
||||||
|
*/
|
||||||
|
private void doSend(List<String> loginNames, String title, String content, String pcUrl, String mobileUrl) {
|
||||||
|
this.doSend(loginNames, title, content, pcUrl, mobileUrl, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** fromUser 为后台任务传入的发送人工号;为空时沿用当前请求身份。发送结果写入日志。 */
|
||||||
|
private void doSend(List<String> loginNames, String title, String content, String pcUrl, String mobileUrl, String fromUser) {
|
||||||
|
if (!Globals.MyConfig.getBoolean("AppSms", false) || !Globals.sso) {
|
||||||
|
log.info("钉钉消息提醒未发送:消息开关未开启或当前为开发模式");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StrUtil.hasBlank(APP_ID, APP_SECRET, COMP_ID, TEMPLATE_ID)) {
|
||||||
|
log.warn("钉钉消息提醒未发送:请补充 appId、appSecret、compId、templateId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loginNames == null || loginNames.isEmpty() || StrUtil.isBlank(content)) {
|
||||||
|
log.warn("钉钉消息提醒未发送:接收人或正文为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> receivers = loginNames.stream().filter(StrUtil::isNotBlank)
|
||||||
|
.map(String::trim).distinct().toList();
|
||||||
|
if (receivers.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String linkUrl;
|
||||||
|
final String token;
|
||||||
|
try {
|
||||||
|
// 协议仅接受一个链接;优先移动端,其次 PC,普通消息使用系统首页。
|
||||||
|
String target = StrUtil.blankToDefault(mobileUrl, StrUtil.blankToDefault(pcUrl, Globals.AppDomain));
|
||||||
|
URI uri = URI.create(target);
|
||||||
|
URI resolved = uri.isAbsolute() ? uri : URI.create(Globals.AppDomain + "/").resolve(uri);
|
||||||
|
if (!("https".equalsIgnoreCase(resolved.getScheme()) || "http".equalsIgnoreCase(resolved.getScheme()))
|
||||||
|
|| StrUtil.isBlank(resolved.getHost())) {
|
||||||
|
throw new IllegalArgumentException("钉钉提醒链接必须为 HTTP/HTTPS 地址");
|
||||||
|
}
|
||||||
|
linkUrl = resolved.toString();
|
||||||
|
token = getAccessToken();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// HTTP 异常可能带有含凭证的 URL,不输出异常正文或 Token。
|
||||||
|
log.error("钉钉消息提醒准备失败,异常类型:{}", e.getClass().getSimpleName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String userId : receivers) {
|
||||||
|
String messageId = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
try {
|
||||||
|
// 外层 content 必须为 JSON 字符串,内层 content 对应模板中的 #{content}。
|
||||||
|
JSONObject params = new JSONObject();
|
||||||
|
params.set("id", messageId);
|
||||||
|
params.set("templateId", TEMPLATE_ID);
|
||||||
|
params.set("title", StrUtil.blankToDefault(title, "智慧工会"));
|
||||||
|
params.set("fromUser", StrUtil.isNotBlank(fromUser) ? fromUser : SecurityUtil.getUserLoginname());
|
||||||
|
params.set("userId", userId);
|
||||||
|
params.set("content", new JSONObject().set("content", content).toString());
|
||||||
|
params.set("detailInfo", new JSONObject().set("linkUrl", linkUrl).set("urlDes", "查看详情"));
|
||||||
|
log.info("钉钉消息提醒参数打印,detailInfo={}", params.toString());
|
||||||
|
JSONObject result = sendMessage(token, params);
|
||||||
|
log.info("钉钉消息提醒请求结果返回,result={}", result.toString());
|
||||||
|
String code = result.getStr("code");
|
||||||
|
// 凭证失效清理缓存并停止批次,下次发送重新获取;网络异常不重发,避免重复提醒。
|
||||||
|
if ("10002".equals(code)) {
|
||||||
|
invalidateToken(token);
|
||||||
|
log.error("钉钉消息提醒认证失效,msgId={},停止本批次发送", messageId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(result.getBool("status")) && ("1".equals(code) || "5".equals(code))) {
|
||||||
|
log.info("钉钉消息提醒发送成功,msgId={},code={}", messageId, code);
|
||||||
|
} else {
|
||||||
|
log.error("钉钉消息提醒发送未成功,msgId={},code={}", messageId, code);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("钉钉消息提醒发送异常,msgId={},异常类型:{}", messageId, e.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回认证响应 result.access_token;result.expires_in 为有效秒数。
|
||||||
|
* 优先读取 Redis,多实例缓存未命中时仅允许持锁者申请,其余请求限时等待缓存。
|
||||||
|
*/
|
||||||
|
private String getAccessToken() {
|
||||||
|
String key = tokenCacheKey();
|
||||||
|
String lockKey = key + ":refresh";
|
||||||
|
String owner = UUID.randomUUID().toString();
|
||||||
|
long deadline = System.nanoTime() + REQUEST_TIMEOUT * 2L * 1_000_000L;
|
||||||
|
while (true) {
|
||||||
|
String cached = redisService.get(key);
|
||||||
|
if (StrUtil.isNotBlank(cached)) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
if ("OK".equals(redisService.set(lockKey, owner, "NX", "EX", TOKEN_LOCK_SECONDS))) {
|
||||||
|
try {
|
||||||
|
// 取锁前其他实例可能刚写入缓存,二次读取避免重复认证。
|
||||||
|
cached = redisService.get(key);
|
||||||
|
return StrUtil.isNotBlank(cached) ? cached : requestAndCacheToken(key, lockKey, owner);
|
||||||
|
} finally {
|
||||||
|
// 仅释放自己的锁;Redis 异常不覆盖认证结果,锁仍会按 TTL 自动过期。
|
||||||
|
try {
|
||||||
|
redisService.eval(DELETE_IF_MATCH, List.of(lockKey), List.of(owner));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("钉钉 Token 刷新锁释放失败,将自动过期,异常类型:{}", e.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (System.nanoTime() >= deadline) {
|
||||||
|
throw new IllegalStateException("等待钉钉 Token 刷新超时");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException("等待钉钉 Token 刷新被中断");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key 为 Token 缓存键,lockKey/owner 为当前刷新锁及归属标识,返回可发送消息的 Token。
|
||||||
|
* TTL 按 expires_in 扣除请求耗时及提前过期窗口计算,失败或锁过期时不写入缓存。
|
||||||
|
*/
|
||||||
|
private String requestAndCacheToken(String key, String lockKey, String owner) {
|
||||||
|
long requestedAt = System.nanoTime();
|
||||||
|
try (HttpResponse response = HttpRequest.get(BASE_URL + "/token")
|
||||||
|
.form("appId", APP_ID).form("appSecret", APP_SECRET).form("compId", COMP_ID)
|
||||||
|
.timeout(REQUEST_TIMEOUT).execute()) {
|
||||||
|
if (!response.isOk()) {
|
||||||
|
throw new IllegalStateException("消息中心认证 HTTP 状态异常");
|
||||||
|
}
|
||||||
|
JSONObject result = JSONUtil.parseObj(response.body());
|
||||||
|
JSONObject data = result.getJSONObject("result");
|
||||||
|
if (!Boolean.TRUE.equals(result.getBool("success")) || !"0".equals(result.getStr("code")) || data == null) {
|
||||||
|
throw new IllegalStateException("消息中心认证失败");
|
||||||
|
}
|
||||||
|
String value = data.getStr("access_token");
|
||||||
|
long expiresIn = data.getLong("expires_in", 0L);
|
||||||
|
if (StrUtil.isBlank(value) || expiresIn <= 0) {
|
||||||
|
throw new IllegalStateException("消息中心认证凭证或有效期为空");
|
||||||
|
}
|
||||||
|
long elapsedSeconds = (System.nanoTime() - requestedAt) / 1_000_000_000L + 1;
|
||||||
|
long ttl = expiresIn - Math.min(60, expiresIn / 10) - elapsedSeconds;
|
||||||
|
if (ttl <= 0 || ttl > Integer.MAX_VALUE) {
|
||||||
|
throw new IllegalStateException("消息中心 Token 缓存有效期不合法");
|
||||||
|
}
|
||||||
|
Object saved = redisService.eval(CACHE_IF_OWNER, List.of(lockKey, key),
|
||||||
|
List.of(owner, String.valueOf(ttl), value));
|
||||||
|
if (!Long.valueOf(1).equals(saved)) {
|
||||||
|
throw new IllegalStateException("消息中心 Token 刷新锁已失效");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** token 为接口报告失效的旧凭证;Lua 原子比较删除,避免误删其他实例刷新的凭证。 */
|
||||||
|
private void invalidateToken(String token) {
|
||||||
|
redisService.eval(DELETE_IF_MATCH, List.of(tokenCacheKey()), List.of(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按应用与单位隔离凭证;花括号确保 Token 和刷新锁在 Redis Cluster 中属于同一槽。 */
|
||||||
|
private String tokenCacheKey() {
|
||||||
|
return RedisConstant.REDIS_KEY_DING_MESSAGE_TOKEN + "{" + APP_ID + ":" + COMP_ID + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token 为 URL 查询凭证,params 为单人消息 JSON;
|
||||||
|
* 返回含 status、code、message、info 的响应对象,调用方继续判断业务发送结果。
|
||||||
|
*/
|
||||||
|
private JSONObject sendMessage(String token, JSONObject params) {
|
||||||
|
String url = BASE_URL + "/message/send?token=" + URLEncoder.encode(token, StandardCharsets.UTF_8);
|
||||||
|
try (HttpResponse response = HttpRequest.post(url).header("Content-Type", "application/json; charset=UTF-8").body(params.toString())
|
||||||
|
.timeout(REQUEST_TIMEOUT).execute()) {
|
||||||
|
if (!response.isOk()) {
|
||||||
|
throw new IllegalStateException("消息中心发送 HTTP 状态异常");
|
||||||
|
}
|
||||||
|
return JSONUtil.parseObj(response.body());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
package com.budwk.app.base.sms.impl.jshvc;
|
|
||||||
|
|
||||||
import com.budwk.app.base.sms.SmsService;
|
|
||||||
import com.budwk.app.todo.platform.TodoPlatformConfig;
|
|
||||||
import com.budwk.app.web.commons.base.Globals;
|
|
||||||
import com.wisedu.casp.sdk.api.mc.ReceiverDto;
|
|
||||||
import com.wisedu.casp.sdk.api.mc.SendMsgRequest;
|
|
||||||
import com.wisedu.casp.sdk.api.mc.SendMsgResponse;
|
|
||||||
import com.wisedu.casp.sdk.api.mc.constants.MsgType_Constants;
|
|
||||||
import com.wisedu.casp.sdk.api.mc.constants.SendType_Constants;
|
|
||||||
import com.wisedu.casp.sdk.api.mc.constants.WxSendType_Constants;
|
|
||||||
import com.wisedu.casp.sdk.api.mc.recallMsg.RecallMsgRequest;
|
|
||||||
import com.wisedu.casp.sdk.api.mc.recallMsg.RecallMsgResponse;
|
|
||||||
import com.wisedu.casp.sdk.core.Client;
|
|
||||||
import com.wisedu.casp.sdk.core.DefaultClient;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
|
||||||
import org.nutz.json.Json;
|
|
||||||
import org.nutz.lang.Lang;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @ClassName SmsJshvcServiceImpl
|
|
||||||
* @Author JyuHsin
|
|
||||||
* @Date 2025/11/26 19:32
|
|
||||||
* @Version 1.0
|
|
||||||
* @Description TODO
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@IocBean
|
|
||||||
public class SmsJshvcServiceImpl implements SmsService {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendSingleMessage(String loginName, String content) {
|
|
||||||
this.sendSingleMessage(loginName, "智慧工会", content);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendSingleMessage(String loginName, String title, String content) {
|
|
||||||
this.massSendMessage(List.of(loginName), title, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendSingleTextCard(String loginName, String content, String pcUrl, String mobileUrl) {
|
|
||||||
this.sendSingleTextCard(loginName, "智慧工会", content, pcUrl, mobileUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendSingleTextCard(String loginName, String title, String content, String pcUrl, String mobileUrl) {
|
|
||||||
this.massSendTextCard(List.of(loginName), title, content, pcUrl, mobileUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendSinglePicTextCard(String loginName, String content, String pcUrl, String mobileUrl, String picUrl) {
|
|
||||||
this.sendSinglePicTextCard(loginName, "智慧工会", content, pcUrl, mobileUrl, picUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendSinglePicTextCard(String loginName, String title, String content, String pcUrl, String mobileUrl, String picUrl) {
|
|
||||||
this.massSendPicTextCard(List.of(loginName), title, content, pcUrl, mobileUrl, picUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void massSendMessage(List<String> loginNames, String content) {
|
|
||||||
this.massSendMessage(loginNames, "智慧工会", content);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void massSendMessage(List<String> loginNames, String title, String content) {
|
|
||||||
List<ReceiverDto> list = loginNames.stream().map(loginName -> {
|
|
||||||
ReceiverDto receiverDto = new ReceiverDto();
|
|
||||||
receiverDto.setUserId(loginName);
|
|
||||||
return receiverDto;
|
|
||||||
}).toList();
|
|
||||||
this.doSend(WxSendType_Constants.TEXT, list, title, content, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void massSendTextCard(List<String> loginNames, String content, String pcUrl, String mobileUrl) {
|
|
||||||
this.massSendTextCard(loginNames, "智慧工会", content, pcUrl, mobileUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void massSendTextCard(List<String> loginNames, String title, String content, String pcUrl, String mobileUrl) {
|
|
||||||
List<ReceiverDto> list = loginNames.stream().map(loginName -> {
|
|
||||||
ReceiverDto receiverDto = new ReceiverDto();
|
|
||||||
receiverDto.setUserId(loginName);
|
|
||||||
return receiverDto;
|
|
||||||
}).toList();
|
|
||||||
this.doSend(WxSendType_Constants.TEXT_CARD, list, title, content, pcUrl, mobileUrl, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void massSendPicTextCard(List<String> loginNames, String content, String pcUrl, String mobileUrl, String picUrl) {
|
|
||||||
this.massSendPicTextCard(loginNames, "智慧工会", content, pcUrl, mobileUrl, picUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void massSendPicTextCard(List<String> loginNames, String title, String content, String pcUrl, String mobileUrl, String picUrl) {
|
|
||||||
List<ReceiverDto> list = loginNames.stream().map(loginName -> {
|
|
||||||
ReceiverDto receiverDto = new ReceiverDto();
|
|
||||||
receiverDto.setUserId(loginName);
|
|
||||||
return receiverDto;
|
|
||||||
}).toList();
|
|
||||||
this.doSend(WxSendType_Constants.NEWS, list, title, content, pcUrl, mobileUrl, picUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void recallMsg(String msgId) {
|
|
||||||
try {
|
|
||||||
Client client = new DefaultClient(TodoPlatformConfig.TODO_PLATFORM_BASE_URL, TodoPlatformConfig.TODO_APP_ID, TodoPlatformConfig.TODO_APP_SECRET);
|
|
||||||
RecallMsgRequest request = new RecallMsgRequest();
|
|
||||||
request.setMsgId(msgId);
|
|
||||||
|
|
||||||
log.info("撤回企业微信消息,请求参数:{}", Json.toJson(request));
|
|
||||||
RecallMsgResponse response = client.execute(request);
|
|
||||||
log.info("撤回企业微信消息,接口响应数据:{}", Json.toJson(response));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("撤回企业微信消息失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doSend(String sendType,
|
|
||||||
List<ReceiverDto> receivers,
|
|
||||||
String title,
|
|
||||||
String content,
|
|
||||||
String pcUrl,
|
|
||||||
String mobileUrl,
|
|
||||||
String picUrl) {
|
|
||||||
if (!Globals.MyConfig.getBoolean("AppSms", false)) {
|
|
||||||
log.error("【log】消息配置未开启");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Globals.sso) {
|
|
||||||
log.error("【log】开发模式不允许发送企业微信通知");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Lang.isEmpty(receivers)) {
|
|
||||||
log.error("send fail by receivers is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// 创建客户端
|
|
||||||
Client client = new DefaultClient(TodoPlatformConfig.TODO_PLATFORM_BASE_URL, TodoPlatformConfig.TODO_APP_ID, TodoPlatformConfig.TODO_APP_SECRET);
|
|
||||||
SendMsgRequest request = new SendMsgRequest();
|
|
||||||
request.setSubject(title);
|
|
||||||
request.setContent(content);
|
|
||||||
request.setMsgType(MsgType_Constants.COMMON);
|
|
||||||
request.setSendType(SendType_Constants.WECHAT_ENTERPRISE);
|
|
||||||
request.setWxSendType(sendType);
|
|
||||||
request.setReceivers(receivers);
|
|
||||||
|
|
||||||
if (WxSendType_Constants.TEXT_CARD.equals(sendType)) {
|
|
||||||
request.setPcUrl(pcUrl);
|
|
||||||
request.setMobileUrl(mobileUrl);
|
|
||||||
request.setUrlDesc("查看详情");
|
|
||||||
}
|
|
||||||
if (WxSendType_Constants.NEWS.equals(sendType)) {
|
|
||||||
request.setPcUrl(pcUrl);
|
|
||||||
request.setMobileUrl(mobileUrl);
|
|
||||||
request.setUrlDesc("查看详情");
|
|
||||||
request.setQyWeChatImgUrl(picUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("发送企业微信通知,请求参数:{}", Json.toJson(request));
|
|
||||||
SendMsgResponse response = client.execute(request);
|
|
||||||
log.info("发送企业微信通知,接口响应数据:{}", Json.toJson(response));
|
|
||||||
|
|
||||||
if (response.isSuccess() && response.getStatus() == 200) {
|
|
||||||
// TODO 消息Id,后面可以存着,撤回要用到
|
|
||||||
String msgId = response.getMsgId();
|
|
||||||
log.info("发送企业微信通知成功: {}, msg: {}", response.getStatus(), response.getMsg());
|
|
||||||
} else {
|
|
||||||
log.info("发送企业微信通知失败:{}", response.getMsg());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("发送企业微信通知失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,9 @@ import org.apache.pdfbox.cos.COSName;
|
|||||||
import org.apache.pdfbox.io.RandomAccessRead;
|
import org.apache.pdfbox.io.RandomAccessRead;
|
||||||
import org.apache.pdfbox.io.RandomAccessReadBuffer;
|
import org.apache.pdfbox.io.RandomAccessReadBuffer;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.mvc.annotation.*;
|
import org.nutz.mvc.annotation.*;
|
||||||
@@ -143,6 +145,43 @@ public class SysFileController {
|
|||||||
return Result.success().addData(sysFileService.convertHtml(file));
|
return Result.success().addData(sysFileService.convertHtml(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将PDF逐页转换为图片富文本。
|
||||||
|
*
|
||||||
|
* @param file 上传的PDF文件
|
||||||
|
* @return Result,data为按PDF页码顺序组成的图片HTML
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckLogin
|
||||||
|
@ApiOperation("文件管理-PDF转换图片富文本")
|
||||||
|
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result convertPdfToImageHtml(TempFile file) {
|
||||||
|
if (file == null || !"pdf".equalsIgnoreCase(FileUtil.extName(file.getSubmittedFileName()))) {
|
||||||
|
return Result.error("请上传PDF文件");
|
||||||
|
}
|
||||||
|
return Result.success().addData(sysFileService.convertPdfToImageHtml(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Excel的每个非空页签转换为独立HTML表格。
|
||||||
|
*
|
||||||
|
* @param file 上传的xls或xlsx文件
|
||||||
|
* @return Result,data为按页签顺序组成的标题及HTML表格
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckLogin
|
||||||
|
@ApiOperation("文件管理-Excel转换表格富文本")
|
||||||
|
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result convertExcelToHtml(TempFile file) {
|
||||||
|
String extension = file == null ? "" : FileUtil.extName(file.getSubmittedFileName());
|
||||||
|
if (!"xls".equalsIgnoreCase(extension) && !"xlsx".equalsIgnoreCase(extension)) {
|
||||||
|
return Result.error("请上传xls或xlsx文件");
|
||||||
|
}
|
||||||
|
return Result.success().addData(sysFileService.convertExcelToHtml(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("文件管理-删除文件")
|
@ApiOperation("文件管理-删除文件")
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
@Column
|
@Column
|
||||||
@Comment("编制类别码")
|
@Comment("编制类别码")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 30)
|
@ColDefine(type = ColType.VARCHAR, width = 30)
|
||||||
@DataCenterColumn(name = "编制类型", key = "BZLB")
|
@DataCenterColumn(name = "编制类型", key = "yrxsmc")
|
||||||
private String preparedBy;
|
private String preparedBy;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@@ -386,4 +386,16 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
@DataCenterColumn(name = "党政职务", key = "DZZW")
|
@DataCenterColumn(name = "党政职务", key = "DZZW")
|
||||||
private String position;
|
private String position;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("人员类别码")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
|
@DataCenterColumn(name = "人员类别码", key = "rylbm")
|
||||||
|
private String rylbm;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("在岗情况码")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
|
@DataCenterColumn(name = "在岗情况码", key = "zgqkm")
|
||||||
|
private String zgqkm;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,4 +71,20 @@ public interface SysFileService extends BaseService<Sys_file> {
|
|||||||
* 转换为HTML
|
* 转换为HTML
|
||||||
*/
|
*/
|
||||||
String convertHtml(TempFile file);
|
String convertHtml(TempFile file);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将PDF逐页转换为图片并生成富文本HTML。
|
||||||
|
*
|
||||||
|
* @param file PDF文件,文件内容不能为空
|
||||||
|
* @return 按PDF页码顺序排列的图片HTML
|
||||||
|
*/
|
||||||
|
String convertPdfToImageHtml(TempFile file);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Excel中的非空页签转换为富文本HTML表格。
|
||||||
|
*
|
||||||
|
* @param file xls或xlsx文件,文件内容不能为空
|
||||||
|
* @return 按页签顺序排列的标题及HTML表格
|
||||||
|
*/
|
||||||
|
String convertExcelToHtml(TempFile file);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
|
|||||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
|
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
|
||||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
|
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Chain;
|
import org.nutz.dao.Chain;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
@@ -163,9 +164,9 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
Cnd cnd = Cnd.where(Sys_user_source::getPullTime, "=", updateParam.getPullTime());
|
Cnd cnd = Cnd.where(Sys_user_source::getPullTime, "=", updateParam.getPullTime());
|
||||||
|
|
||||||
// 处理复杂条件
|
// 处理复杂条件
|
||||||
if (updateParam.getConditionGroup() != null) {
|
// if (updateParam.getConditionGroup() != null) {
|
||||||
// cnd = ConditionGroupUtil.applyConditionGroup(cnd, updateParam.getConditionGroup());
|
// cnd = ConditionGroupUtil.applyConditionGroup(cnd, updateParam.getConditionGroup());
|
||||||
}
|
// }
|
||||||
|
|
||||||
List<Sys_user_source> sources = dao.query(Sys_user_source.class, cnd.groupBy("loginname"));
|
List<Sys_user_source> sources = dao.query(Sys_user_source.class, cnd.groupBy("loginname"));
|
||||||
log.info("符合条件的数据源记录数: {}", sources.size());
|
log.info("符合条件的数据源记录数: {}", sources.size());
|
||||||
@@ -181,6 +182,9 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
List<String> addMemberUserIds = Collections.synchronizedList(new ArrayList<>());
|
List<String> addMemberUserIds = Collections.synchronizedList(new ArrayList<>());
|
||||||
List<String> removeMemberUserIds = Collections.synchronizedList(new ArrayList<>());
|
List<String> removeMemberUserIds = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
List<String> preparedByList = List.of("人才市场派遣", "人力资源服务中心派遣", "海川派遣", "越仕中心", "柔性引进");
|
||||||
|
List<String> rylbmList = List.of("退休返聘", "临聘");
|
||||||
|
|
||||||
// 处理每条数据
|
// 处理每条数据
|
||||||
for (Sys_user_source source : sources) {
|
for (Sys_user_source source : sources) {
|
||||||
Sys_user user = userMap.get(source.getLoginname());
|
Sys_user user = userMap.get(source.getLoginname());
|
||||||
@@ -195,8 +199,11 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
u.setSalt(salt);
|
u.setSalt(salt);
|
||||||
u.setPassword(PwdUtil.getPassword(PwdUtil.generate(12), salt));
|
u.setPassword(PwdUtil.getPassword(PwdUtil.generate(12), salt));
|
||||||
|
|
||||||
if (u.getMember()) {
|
if (u.getUserState().equals("在职")
|
||||||
//addMemberUserIds.add(u.getId());
|
&& (u.getPersonType().equals("在编") || preparedByList.contains(u.getPreparedBy()))
|
||||||
|
&& (StringUtils.isBlank(u.getRylbm()) || !rylbmList.contains(u.getRylbm()))) {
|
||||||
|
addMemberUserIds.add(u.getId());
|
||||||
|
u.setMember(true);
|
||||||
}
|
}
|
||||||
needInitUserList.add(u);
|
needInitUserList.add(u);
|
||||||
} else {
|
} else {
|
||||||
@@ -205,12 +212,16 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
u.setMember(null);
|
u.setMember(null);
|
||||||
u.setMemberTime(null);
|
u.setMemberTime(null);
|
||||||
// 更新会员状态
|
// 更新会员状态
|
||||||
boolean currentIsMember = user.getMember() != null && user.getMember();
|
if (u.getUserState().equals("在职")
|
||||||
|
&& (u.getPersonType().equals("在编") || preparedByList.contains(u.getPreparedBy()))
|
||||||
if (!currentIsMember) {
|
&& (StringUtils.isBlank(u.getRylbm()) || !rylbmList.contains(u.getRylbm()))) {
|
||||||
//addMemberUserIds.add(user.getId());
|
if (!user.getMember()) {
|
||||||
|
addMemberUserIds.add(u.getId());
|
||||||
|
u.setMember(true);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
//removeMemberUserIds.add(user.getId());
|
removeMemberUserIds.add(user.getId());
|
||||||
|
u.setMember(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
needDoUpdateList.add(u);
|
needDoUpdateList.add(u);
|
||||||
|
|||||||
@@ -23,16 +23,53 @@ import com.budwk.app.sys.services.SysFileService;
|
|||||||
import com.budwk.app.sys.utils.SysFileLocalUtil;
|
import com.budwk.app.sys.utils.SysFileLocalUtil;
|
||||||
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.cos.COSDictionary;
|
||||||
|
import org.apache.pdfbox.cos.COSName;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
|
import org.apache.poi.hssf.usermodel.HSSFClientAnchor;
|
||||||
|
import org.apache.poi.hssf.usermodel.HSSFPatriarch;
|
||||||
|
import org.apache.poi.hssf.usermodel.HSSFPicture;
|
||||||
|
import org.apache.poi.hssf.usermodel.HSSFShape;
|
||||||
|
import org.apache.poi.hssf.usermodel.HSSFSheet;
|
||||||
|
import org.apache.poi.hssf.util.HSSFColor;
|
||||||
|
import org.apache.poi.ss.usermodel.BorderStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.Cell;
|
||||||
|
import org.apache.poi.ss.usermodel.CellStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||||
|
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||||
|
import org.apache.poi.ss.usermodel.Font;
|
||||||
|
import org.apache.poi.ss.usermodel.FormulaEvaluator;
|
||||||
|
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||||
|
import org.apache.poi.ss.usermodel.PictureData;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.VerticalAlignment;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
|
import org.apache.poi.ss.util.CellRangeAddress;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFDrawing;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFPicture;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFShape;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.json.Json;
|
import org.nutz.json.Json;
|
||||||
|
import org.nutz.lang.Strings;
|
||||||
import org.nutz.lang.random.R;
|
import org.nutz.lang.random.R;
|
||||||
import org.nutz.mvc.upload.TempFile;
|
import org.nutz.mvc.upload.TempFile;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -41,7 +78,12 @@ import java.math.RoundingMode;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -199,6 +241,370 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public String convertPdfToImageHtml(TempFile file) {
|
||||||
|
validateFileExtension(file, List.of("pdf"), "请上传PDF文件");
|
||||||
|
try (PDDocument document = Loader.loadPDF(file.getFile())) {
|
||||||
|
validatePdfSafety(document);
|
||||||
|
if (document.getNumberOfPages() < 1) {
|
||||||
|
throw new BaseException("PDF文件中没有可导入的页面");
|
||||||
|
}
|
||||||
|
|
||||||
|
PDFRenderer renderer = new PDFRenderer(document);
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
for (int pageIndex = 0; pageIndex < document.getNumberOfPages(); pageIndex++) {
|
||||||
|
// 144 DPI兼顾文字清晰度和图片体积,页面顺序与原PDF保持一致。
|
||||||
|
BufferedImage pageImage = renderer.renderImageWithDPI(pageIndex, 144, ImageType.RGB);
|
||||||
|
try (ByteArrayOutputStream imageOutput = new ByteArrayOutputStream()) {
|
||||||
|
ImageIO.write(pageImage, "png", imageOutput);
|
||||||
|
String imageUrl = storageFile(SysFileEngineTypeEnum.MINIO.getValue(),
|
||||||
|
R.UU32() + ".png", imageOutput.toByteArray(), false);
|
||||||
|
html.append("<p style=\"margin:0 0 12px;\">")
|
||||||
|
.append("<img src=\"").append(imageUrl)
|
||||||
|
.append("\" style=\"display:block;max-width:100%;height:auto;margin:0 auto;\">")
|
||||||
|
.append("</p>");
|
||||||
|
} finally {
|
||||||
|
pageImage.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html.toString();
|
||||||
|
} catch (BaseException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("PDF转换图片异常", e);
|
||||||
|
throw new BaseException("PDF文件读取失败,请确认文件未损坏");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public String convertExcelToHtml(TempFile file) {
|
||||||
|
validateFileExtension(file, List.of("xls", "xlsx"), "请上传xls或xlsx文件");
|
||||||
|
try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) {
|
||||||
|
DataFormatter formatter = new DataFormatter();
|
||||||
|
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
int tableCount = 0;
|
||||||
|
|
||||||
|
for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) {
|
||||||
|
Sheet sheet = workbook.getSheetAt(sheetIndex);
|
||||||
|
String tableHtml = buildSheetHtml(workbook, sheet, formatter, evaluator);
|
||||||
|
if (StrUtil.isBlank(tableHtml)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tableCount++;
|
||||||
|
html.append("<div style=\"margin-bottom:20px;\">")
|
||||||
|
.append("<h3 style=\"margin:0 0 10px;font-size:16px;\">")
|
||||||
|
.append(Strings.escapeHtml(sheet.getSheetName()))
|
||||||
|
.append("</h3>")
|
||||||
|
.append("<div style=\"max-width:100%;overflow-x:auto;\">")
|
||||||
|
.append(tableHtml)
|
||||||
|
.append("</div></div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableCount < 1) {
|
||||||
|
throw new BaseException("Excel文件中没有可导入的表格内容");
|
||||||
|
}
|
||||||
|
return html.toString();
|
||||||
|
} catch (BaseException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Excel转换HTML异常", e);
|
||||||
|
throw new BaseException("Excel文件读取失败,请确认文件格式正确且未损坏");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将单个非空页签转换为HTML表格,合并单元格使用rowspan和colspan还原。
|
||||||
|
*/
|
||||||
|
private String buildSheetHtml(Workbook workbook, Sheet sheet, DataFormatter formatter,
|
||||||
|
FormulaEvaluator evaluator) {
|
||||||
|
List<ExcelEmbeddedImage> embeddedImages = extractSheetImages(sheet);
|
||||||
|
int minRow = Integer.MAX_VALUE;
|
||||||
|
int maxRow = -1;
|
||||||
|
int minColumn = Integer.MAX_VALUE;
|
||||||
|
int maxColumn = -1;
|
||||||
|
|
||||||
|
for (Row row : sheet) {
|
||||||
|
for (Cell cell : row) {
|
||||||
|
if (StrUtil.isBlank(getCellDisplayValue(cell, formatter, evaluator))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
minRow = Math.min(minRow, cell.getRowIndex());
|
||||||
|
maxRow = Math.max(maxRow, cell.getRowIndex());
|
||||||
|
minColumn = Math.min(minColumn, cell.getColumnIndex());
|
||||||
|
maxColumn = Math.max(maxColumn, cell.getColumnIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Excel图片存放在绘图层,即使锚定单元格没有文字,也必须纳入表格范围。
|
||||||
|
for (ExcelEmbeddedImage image : embeddedImages) {
|
||||||
|
minRow = Math.min(minRow, image.rowIndex);
|
||||||
|
maxRow = Math.max(maxRow, image.rowIndex);
|
||||||
|
minColumn = Math.min(minColumn, image.columnIndex);
|
||||||
|
maxColumn = Math.max(maxColumn, image.columnIndex);
|
||||||
|
}
|
||||||
|
if (maxRow < 0 || maxColumn < 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容位于合并区域时,将区域边界纳入表格范围,避免合并内容被截断。
|
||||||
|
for (CellRangeAddress range : sheet.getMergedRegions()) {
|
||||||
|
if (range.getFirstRow() <= maxRow && range.getLastRow() >= minRow
|
||||||
|
&& range.getFirstColumn() <= maxColumn && range.getLastColumn() >= minColumn) {
|
||||||
|
minRow = Math.min(minRow, range.getFirstRow());
|
||||||
|
maxRow = Math.max(maxRow, range.getLastRow());
|
||||||
|
minColumn = Math.min(minColumn, range.getFirstColumn());
|
||||||
|
maxColumn = Math.max(maxColumn, range.getLastColumn());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, CellRangeAddress> mergedStarts = new HashMap<>();
|
||||||
|
Set<String> mergedCovered = new HashSet<>();
|
||||||
|
for (CellRangeAddress range : sheet.getMergedRegions()) {
|
||||||
|
if (range.getLastRow() < minRow || range.getFirstRow() > maxRow
|
||||||
|
|| range.getLastColumn() < minColumn || range.getFirstColumn() > maxColumn) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mergedStarts.put(cellKey(range.getFirstRow(), range.getFirstColumn()), range);
|
||||||
|
for (int rowIndex = range.getFirstRow(); rowIndex <= range.getLastRow(); rowIndex++) {
|
||||||
|
for (int columnIndex = range.getFirstColumn(); columnIndex <= range.getLastColumn(); columnIndex++) {
|
||||||
|
if (rowIndex != range.getFirstRow() || columnIndex != range.getFirstColumn()) {
|
||||||
|
mergedCovered.add(cellKey(rowIndex, columnIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> imageUrlsByCell = new HashMap<>();
|
||||||
|
for (ExcelEmbeddedImage image : embeddedImages) {
|
||||||
|
int imageRow = image.rowIndex;
|
||||||
|
int imageColumn = image.columnIndex;
|
||||||
|
for (CellRangeAddress range : sheet.getMergedRegions()) {
|
||||||
|
if (range.isInRange(imageRow, imageColumn)) {
|
||||||
|
imageRow = range.getFirstRow();
|
||||||
|
imageColumn = range.getFirstColumn();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageUrlsByCell.computeIfAbsent(cellKey(imageRow, imageColumn), key -> new ArrayList<>())
|
||||||
|
.add(image.imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder html = new StringBuilder("<table style=\"border-collapse:collapse;width:100%;table-layout:auto;\">");
|
||||||
|
for (int rowIndex = minRow; rowIndex <= maxRow; rowIndex++) {
|
||||||
|
Row row = sheet.getRow(rowIndex);
|
||||||
|
html.append("<tr>");
|
||||||
|
for (int columnIndex = minColumn; columnIndex <= maxColumn; columnIndex++) {
|
||||||
|
String key = cellKey(rowIndex, columnIndex);
|
||||||
|
if (mergedCovered.contains(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Cell cell = row == null ? null : row.getCell(columnIndex);
|
||||||
|
CellRangeAddress range = mergedStarts.get(key);
|
||||||
|
html.append("<td");
|
||||||
|
if (range != null) {
|
||||||
|
int rowSpan = range.getLastRow() - range.getFirstRow() + 1;
|
||||||
|
int columnSpan = range.getLastColumn() - range.getFirstColumn() + 1;
|
||||||
|
if (rowSpan > 1) {
|
||||||
|
html.append(" rowspan=\"").append(rowSpan).append("\"");
|
||||||
|
}
|
||||||
|
if (columnSpan > 1) {
|
||||||
|
html.append(" colspan=\"").append(columnSpan).append("\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html.append(" style=\"").append(buildCellStyle(workbook, cell)).append("\">")
|
||||||
|
.append(buildCellHtml(cell, formatter, evaluator, imageUrlsByCell.get(key)))
|
||||||
|
.append("</td>");
|
||||||
|
}
|
||||||
|
html.append("</tr>");
|
||||||
|
}
|
||||||
|
return html.append("</table>").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取页签绘图层中的嵌入图片,图片锚点用于确定其所属单元格。
|
||||||
|
*/
|
||||||
|
private List<ExcelEmbeddedImage> extractSheetImages(Sheet sheet) {
|
||||||
|
List<ExcelEmbeddedImage> images = new ArrayList<>();
|
||||||
|
if (sheet instanceof XSSFSheet xssfSheet) {
|
||||||
|
XSSFDrawing drawing = xssfSheet.getDrawingPatriarch();
|
||||||
|
if (drawing != null) {
|
||||||
|
for (XSSFShape shape : drawing.getShapes()) {
|
||||||
|
if (shape instanceof XSSFPicture picture && picture.getAnchor() instanceof XSSFClientAnchor anchor) {
|
||||||
|
addExcelEmbeddedImage(images, anchor.getRow1(), anchor.getCol1(), picture.getPictureData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (sheet instanceof HSSFSheet hssfSheet) {
|
||||||
|
HSSFPatriarch drawing = hssfSheet.getDrawingPatriarch();
|
||||||
|
if (drawing != null) {
|
||||||
|
for (HSSFShape shape : drawing.getChildren()) {
|
||||||
|
if (shape instanceof HSSFPicture picture && picture.getAnchor() instanceof HSSFClientAnchor anchor) {
|
||||||
|
addExcelEmbeddedImage(images, anchor.getRow1(), anchor.getCol1(), picture.getPictureData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Excel嵌入图片写入现有文件存储,并记录图片对应的单元格位置。
|
||||||
|
*/
|
||||||
|
private void addExcelEmbeddedImage(List<ExcelEmbeddedImage> images, int rowIndex, int columnIndex,
|
||||||
|
PictureData pictureData) {
|
||||||
|
if (pictureData == null || pictureData.getData() == null || pictureData.getData().length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String extension = StrUtil.blankToDefault(pictureData.suggestFileExtension(), "png")
|
||||||
|
.replaceAll("[^a-zA-Z0-9]", "");
|
||||||
|
if (StrUtil.isBlank(extension)) {
|
||||||
|
extension = "png";
|
||||||
|
}
|
||||||
|
String imageUrl = storageFile(SysFileEngineTypeEnum.MINIO.getValue(),
|
||||||
|
R.UU32() + "." + extension.toLowerCase(), pictureData.getData(), false);
|
||||||
|
images.add(new ExcelEmbeddedImage(rowIndex, columnIndex, imageUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCellDisplayValue(Cell cell, DataFormatter formatter, FormulaEvaluator evaluator) {
|
||||||
|
if (cell == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return formatter.formatCellValue(cell, evaluator);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return formatter.formatCellValue(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCellHtml(Cell cell, DataFormatter formatter, FormulaEvaluator evaluator) {
|
||||||
|
String value = Strings.escapeHtml(getCellDisplayValue(cell, formatter, evaluator));
|
||||||
|
return StrUtil.isBlank(value) ? " " : value.replace("\r\n", "<br>").replace("\n", "<br>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildCellHtml(Cell cell, DataFormatter formatter, FormulaEvaluator evaluator,
|
||||||
|
List<String> imageUrls) {
|
||||||
|
String textHtml = formatCellHtml(cell, formatter, evaluator);
|
||||||
|
if (imageUrls == null || imageUrls.isEmpty()) {
|
||||||
|
return textHtml;
|
||||||
|
}
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
if (!" ".equals(textHtml)) {
|
||||||
|
html.append("<div style=\"margin-bottom:6px;\">").append(textHtml).append("</div>");
|
||||||
|
}
|
||||||
|
for (String imageUrl : imageUrls) {
|
||||||
|
html.append("<img src=\"").append(imageUrl)
|
||||||
|
.append("\" style=\"display:block;max-width:100%;max-height:240px;height:auto;margin:4px auto;object-fit:contain;\">");
|
||||||
|
}
|
||||||
|
return html.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildCellStyle(Workbook workbook, Cell cell) {
|
||||||
|
StringBuilder styleText = new StringBuilder("padding:6px 8px;border:1px solid #dcdfe6;");
|
||||||
|
if (cell == null) {
|
||||||
|
return styleText.toString();
|
||||||
|
}
|
||||||
|
CellStyle cellStyle = cell.getCellStyle();
|
||||||
|
HorizontalAlignment alignment = cellStyle.getAlignment();
|
||||||
|
if (alignment == HorizontalAlignment.CENTER || alignment == HorizontalAlignment.CENTER_SELECTION) {
|
||||||
|
styleText.append("text-align:center;");
|
||||||
|
} else if (alignment == HorizontalAlignment.RIGHT) {
|
||||||
|
styleText.append("text-align:right;");
|
||||||
|
} else {
|
||||||
|
styleText.append("text-align:left;");
|
||||||
|
}
|
||||||
|
VerticalAlignment verticalAlignment = cellStyle.getVerticalAlignment();
|
||||||
|
if (verticalAlignment == VerticalAlignment.TOP) {
|
||||||
|
styleText.append("vertical-align:top;");
|
||||||
|
} else if (verticalAlignment == VerticalAlignment.BOTTOM) {
|
||||||
|
styleText.append("vertical-align:bottom;");
|
||||||
|
} else {
|
||||||
|
styleText.append("vertical-align:middle;");
|
||||||
|
}
|
||||||
|
styleText.append(cellStyle.getWrapText() ? "white-space:pre-wrap;" : "white-space:nowrap;");
|
||||||
|
|
||||||
|
Font font = workbook.getFontAt(cellStyle.getFontIndex());
|
||||||
|
if (font.getBold()) {
|
||||||
|
styleText.append("font-weight:bold;");
|
||||||
|
}
|
||||||
|
if (font.getItalic()) {
|
||||||
|
styleText.append("font-style:italic;");
|
||||||
|
}
|
||||||
|
if (font.getFontHeightInPoints() > 0) {
|
||||||
|
styleText.append("font-size:").append(font.getFontHeightInPoints()).append("pt;");
|
||||||
|
}
|
||||||
|
appendIndexedColor(styleText, "color", font.getColor());
|
||||||
|
if (cellStyle.getFillPattern() != FillPatternType.NO_FILL) {
|
||||||
|
appendIndexedColor(styleText, "background-color", cellStyle.getFillForegroundColor());
|
||||||
|
}
|
||||||
|
if (cellStyle.getBorderTop() != BorderStyle.NONE || cellStyle.getBorderRight() != BorderStyle.NONE
|
||||||
|
|| cellStyle.getBorderBottom() != BorderStyle.NONE || cellStyle.getBorderLeft() != BorderStyle.NONE) {
|
||||||
|
styleText.append("border:1px solid #909399;");
|
||||||
|
}
|
||||||
|
return styleText.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendIndexedColor(StringBuilder styleText, String cssName, short colorIndex) {
|
||||||
|
try {
|
||||||
|
HSSFColor color = HSSFColor.getIndexHash().get(colorIndex);
|
||||||
|
if (color != null) {
|
||||||
|
short[] rgb = color.getTriplet();
|
||||||
|
styleText.append(cssName).append(":#")
|
||||||
|
.append(String.format("%02x%02x%02x", rgb[0], rgb[1], rgb[2]))
|
||||||
|
.append(";");
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// 自定义主题色无法直接映射时保留浏览器默认颜色,不影响表格内容导入。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cellKey(int rowIndex, int columnIndex) {
|
||||||
|
return rowIndex + "_" + columnIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excel嵌入图片及其左上角锚点信息。
|
||||||
|
*/
|
||||||
|
private static class ExcelEmbeddedImage {
|
||||||
|
private final int rowIndex;
|
||||||
|
private final int columnIndex;
|
||||||
|
private final String imageUrl;
|
||||||
|
|
||||||
|
private ExcelEmbeddedImage(int rowIndex, int columnIndex, String imageUrl) {
|
||||||
|
this.rowIndex = rowIndex;
|
||||||
|
this.columnIndex = columnIndex;
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateFileExtension(TempFile file, List<String> allowedExtensions, String errorMessage) {
|
||||||
|
if (file == null || file.getFile() == null || !file.getFile().exists()) {
|
||||||
|
throw new BaseException("文件内容为空");
|
||||||
|
}
|
||||||
|
String extension = FileUtil.extName(file.getSubmittedFileName());
|
||||||
|
if (StrUtil.isBlank(extension) || !allowedExtensions.contains(extension.toLowerCase())) {
|
||||||
|
throw new BaseException(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePdfSafety(PDDocument document) {
|
||||||
|
COSDictionary catalog = document.getDocumentCatalog().getCOSObject();
|
||||||
|
if (catalog.containsKey(COSName.NAMES)) {
|
||||||
|
COSDictionary names = (COSDictionary) catalog.getDictionaryObject(COSName.NAMES);
|
||||||
|
if (names != null && names.containsKey(COSName.JAVA_SCRIPT)) {
|
||||||
|
throw new BaseException("系统检测到此PDF文件具有一定危险性");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (catalog.containsKey(COSName.OPEN_ACTION)) {
|
||||||
|
Object openAction = catalog.getDictionaryObject(COSName.OPEN_ACTION);
|
||||||
|
if (openAction instanceof COSDictionary action
|
||||||
|
&& COSName.JAVA_SCRIPT.equals(action.getDictionaryObject(COSName.S))) {
|
||||||
|
throw new BaseException("系统检测到此PDF文件具有一定危险性");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 存储文件
|
* 存储文件
|
||||||
**/
|
**/
|
||||||
|
|||||||
@@ -57,6 +57,6 @@ public class SysUserAllRenewJob implements Job {
|
|||||||
param.setUpdateMode(SysDataUpdateMode.ALL.name());
|
param.setUpdateMode(SysDataUpdateMode.ALL.name());
|
||||||
param.setConditionGroup(conditionGroup);
|
param.setConditionGroup(conditionGroup);
|
||||||
|
|
||||||
// sysDataUserUpdateService.update(param);
|
sysDataUserUpdateService.update(param);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-8
@@ -115,7 +115,7 @@ public class ActivityBasicScopeController {
|
|||||||
""");
|
""");
|
||||||
try {
|
try {
|
||||||
Cnd cnd = getCnd(pageForm);
|
Cnd cnd = getCnd(pageForm);
|
||||||
cnd.and("u.member", "=", 1);
|
// cnd.and("u.member", "=", 1);
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
@@ -199,7 +199,7 @@ public class ActivityBasicScopeController {
|
|||||||
$condition
|
$condition
|
||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
if (!StpUtil.hasRole(RoleConstant.SYSADMIN.name()) && !StpUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
if (!canViewAllScopeUsers()) {
|
||||||
SqlExpressionGroup group = new SqlExpressionGroup();
|
SqlExpressionGroup group = new SqlExpressionGroup();
|
||||||
group.or("creator", "=", SecurityUtil.getUserId());
|
group.or("creator", "=", SecurityUtil.getUserId());
|
||||||
if(AuthUtil.hasRole(RoleConstant.CLUB_PRESIDENT.name())) {
|
if(AuthUtil.hasRole(RoleConstant.CLUB_PRESIDENT.name())) {
|
||||||
@@ -234,16 +234,17 @@ public class ActivityBasicScopeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断用户权限及获取工会id
|
* 获取参加人员范围弹窗所需的当前用户角色和所属工会信息。
|
||||||
|
* 无请求参数;返回 Result,data 中包含校工会管理员、校工会活动管理员、分工会角色、
|
||||||
|
* 系统管理员判断结果及当前用户所属工会 id,供前端控制全校人员筛选条件。
|
||||||
*
|
*
|
||||||
* @return java.lang.Object
|
* @return 当前用户的活动人员范围权限信息
|
||||||
* @author zhf
|
|
||||||
* @description
|
|
||||||
*/
|
*/
|
||||||
@At
|
@At
|
||||||
public Result getRolesAndUnion() {
|
public Result getRolesAndUnion() {
|
||||||
return Result.success(new NutMap() {{
|
return Result.success(new NutMap() {{
|
||||||
addv("is_A06", StpUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name()));
|
addv("is_A06", StpUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name()));
|
||||||
|
addv("is_school_union_activity_admin", StpUtil.hasRole(RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name()));
|
||||||
addv("is_H04", AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name(), RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_WENTI_SPORTS.name(), RoleConstant.BRANCH_UNION_XJSHAN_PROMOTION.name(), RoleConstant.BRANCH_UNION_WENTI_WY.name()));
|
addv("is_H04", AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name(), RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_WENTI_SPORTS.name(), RoleConstant.BRANCH_UNION_XJSHAN_PROMOTION.name(), RoleConstant.BRANCH_UNION_WENTI_WY.name()));
|
||||||
addv("is_sysadmin", StpUtil.hasRole(RoleConstant.SYSADMIN.name()));
|
addv("is_sysadmin", StpUtil.hasRole(RoleConstant.SYSADMIN.name()));
|
||||||
addv("unionid", SecurityUtil.getUnionId());
|
addv("unionid", SecurityUtil.getUnionId());
|
||||||
@@ -377,7 +378,7 @@ public class ActivityBasicScopeController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())){
|
if (!canViewAllScopeUsers()) {
|
||||||
cnd.andEX("u.unionid", "=", SecurityUtil.getUnionId());
|
cnd.andEX("u.unionid", "=", SecurityUtil.getUnionId());
|
||||||
}
|
}
|
||||||
cnd.andEX("u.unionid", EQ_OR_NEQ_OP, activityUserScopePageParam.getUnionId());
|
cnd.andEX("u.unionid", EQ_OR_NEQ_OP, activityUserScopePageParam.getUnionId());
|
||||||
@@ -393,11 +394,25 @@ public class ActivityBasicScopeController {
|
|||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
cnd.and("u.member", "=", 1);
|
// cnd.and("u.member", "=", 1);
|
||||||
return cnd;
|
return cnd;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否拥有活动参加人员范围的全校人员查看权限。
|
||||||
|
* 系统管理员、校工会管理员和校工会活动管理员可查看全部人员,其他角色仍按所属工会限制。
|
||||||
|
*
|
||||||
|
* @return true 表示可查看全校人员,false 表示仅可查看所属工会人员
|
||||||
|
*/
|
||||||
|
private boolean canViewAllScopeUsers() {
|
||||||
|
return AuthUtil.hasRoleOr(
|
||||||
|
RoleConstant.SYSADMIN.name(),
|
||||||
|
RoleConstant.SCHOOL_UNION_ADMIN.name(),
|
||||||
|
RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@Ok("void")
|
@Ok("void")
|
||||||
|
|||||||
+12
-2
@@ -71,7 +71,15 @@ public class ActivityBasicScopeUserDataController {
|
|||||||
public void index() {
|
public void index() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询已加入活动分组的人员。
|
||||||
|
* pageForm 传递分页、排序和姓名/工号关键字;unionId、unitId、personType、userState、groupId
|
||||||
|
* 分别传递工会、单位、教职工类别、在职状态和活动分组筛选值;existsLoginNameRedisKey
|
||||||
|
* 用于限制导入核对产生的工号集合。返回 Result,data 为包含 list 和 totalCount 的分页结果。
|
||||||
|
* 校工会活动管理员可查看全部活动分组人员,但删除权限仍沿用原有创建人限制。
|
||||||
|
*
|
||||||
|
* @return 活动分组人员分页结果
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("activity.basic.user")
|
@SaCheckPermission("activity.basic.user")
|
||||||
public Result pageData(PageForm pageForm,
|
public Result pageData(PageForm pageForm,
|
||||||
@@ -116,8 +124,10 @@ public class ActivityBasicScopeUserDataController {
|
|||||||
cnd.andEX("u.unitid", "=", unitId);
|
cnd.andEX("u.unitid", "=", unitId);
|
||||||
cnd.andEX("u.personType", "=", personType);
|
cnd.andEX("u.personType", "=", personType);
|
||||||
cnd.andEX("u.userState", "=", userState);
|
cnd.andEX("u.userState", "=", userState);
|
||||||
|
// 校工会活动管理员在设置参加人员范围时,可查看其他管理员已建立的人员分组。
|
||||||
if (!StpUtil.hasRole(RoleConstant.SYSADMIN.name()) &&
|
if (!StpUtil.hasRole(RoleConstant.SYSADMIN.name()) &&
|
||||||
!StpUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
!StpUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name()) &&
|
||||||
|
!StpUtil.hasRole(RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name())) {
|
||||||
cnd.and("aus.creator", "=", SecurityUtil.getUserId());
|
cnd.and("aus.creator", "=", SecurityUtil.getUserId());
|
||||||
}
|
}
|
||||||
if (StrUtil.isNotBlank(existsLoginNameRedisKey)) {
|
if (StrUtil.isNotBlank(existsLoginNameRedisKey)) {
|
||||||
|
|||||||
+45
@@ -14,6 +14,7 @@ import com.budwk.app.sys.models.Sys_unit;
|
|||||||
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
|
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
|
||||||
import com.budwk.app.zhgh.club.model.SysClub;
|
import com.budwk.app.zhgh.club.model.SysClub;
|
||||||
|
import com.budwk.app.zhgh.welfare.service.WelfareProjectService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -22,6 +23,8 @@ import org.nutz.dao.Cnd;
|
|||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
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.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
@@ -52,6 +55,8 @@ public class TrainSignUpManageController {
|
|||||||
private TrainSignUpActivityService trainSignUpActivityManageService;
|
private TrainSignUpActivityService trainSignUpActivityManageService;
|
||||||
@Inject
|
@Inject
|
||||||
private Dao dao;
|
private Dao dao;
|
||||||
|
@Inject
|
||||||
|
private WelfareProjectService welfareProjectService;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@SaCheckPermission("trainSignUp.manage")
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
@@ -90,6 +95,34 @@ public class TrainSignUpManageController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 接收主活动和子活动 ID;Result.data 为当前预计接收人数。 */
|
||||||
|
@At
|
||||||
|
@ApiOperation("查询子活动报名提醒人数")
|
||||||
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
|
public Result reminderRecipientCount(@Param("activityId") String activityId, @Param("courseId") String courseId) {
|
||||||
|
try {
|
||||||
|
return Result.success().addData(trainSignUpActivityManageService.countReminderRecipients(activityId, courseId));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接收主活动、子活动 ID 和提醒正文;成功表示已提交后台发送,data 为本批次人数。 */
|
||||||
|
@At
|
||||||
|
@ApiOperation("提交子活动报名提醒")
|
||||||
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
|
@SLog(tag = "品牌活动-活动管理", msg = "提交子活动报名提醒")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result submitSignupReminder(@Param("activityId") String activityId, @Param("courseId") String courseId,
|
||||||
|
@Param("content") String content) {
|
||||||
|
try {
|
||||||
|
int count = trainSignUpActivityManageService.submitSignupReminder(activityId, courseId, content);
|
||||||
|
return Result.success("已提交发送,接收人数:" + count).addData(count);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("活动状态变更")
|
@ApiOperation("活动状态变更")
|
||||||
@SaCheckPermission("trainSignUp.manage")
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
@@ -153,13 +186,25 @@ public class TrainSignUpManageController {
|
|||||||
@ApiOperation("品牌活动新增/修改")
|
@ApiOperation("品牌活动新增/修改")
|
||||||
@SaCheckPermission("trainSignUp.manage")
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
@SLog(tag = "品牌活动-活动管理", msg = "新增/修改活动")
|
@SLog(tag = "品牌活动-活动管理", msg = "新增/修改活动")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result doHandle(TrainSignUpActivity activity) {
|
public Result doHandle(TrainSignUpActivity activity) {
|
||||||
|
try {
|
||||||
if (StrUtil.isBlank(activity.getId())) {
|
if (StrUtil.isBlank(activity.getId())) {
|
||||||
trainSignUpActivityManageService.add(activity, null);
|
trainSignUpActivityManageService.add(activity, null);
|
||||||
} else {
|
} else {
|
||||||
trainSignUpActivityManageService.edit(activity);
|
trainSignUpActivityManageService.edit(activity);
|
||||||
}
|
}
|
||||||
return Result.success();
|
return Result.success();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.error(99, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@ApiOperation("查询签到扩展可关联的福利项目")
|
||||||
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
|
public Result getWelfareProjects(Integer year) {
|
||||||
|
return Result.success(welfareProjectService.getWelfareList(year));
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
|
|||||||
+58
-1
@@ -10,6 +10,7 @@ import com.budwk.app.base.result.Result;
|
|||||||
import com.budwk.app.base.utils.DistanceUtil;
|
import com.budwk.app.base.utils.DistanceUtil;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpCourse;
|
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpCourse;
|
||||||
|
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpActivity;
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpUserCourse;
|
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpUserCourse;
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
|
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
@@ -23,6 +24,7 @@ import org.nutz.dao.sql.Sql;
|
|||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.Lang;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
@@ -75,6 +77,18 @@ public class TrainsignUpMineController {
|
|||||||
@SaCheckPermission(value = {"trainSignUp.mine", "h5.trainSignUp.mine"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"trainSignUp.mine", "h5.trainSignUp.mine"}, mode = SaMode.OR)
|
||||||
@SLog(tag = "品牌活动-活动签到", msg = "主动扫码签到")
|
@SLog(tag = "品牌活动-活动签到", msg = "主动扫码签到")
|
||||||
public Result drivingScan(String courseId, Double lat, Double lng) {
|
public Result drivingScan(String courseId, Double lat, Double lng) {
|
||||||
|
TrainSignUpCourse extensionCourse = dao.fetch(TrainSignUpCourse.class, courseId);
|
||||||
|
TrainSignUpActivity extensionActivity = extensionCourse == null ? null
|
||||||
|
: dao.fetch(TrainSignUpActivity.class, extensionCourse.getActivityId());
|
||||||
|
if (extensionActivity != null && extensionActivity.getSignPointCount() != null
|
||||||
|
&& extensionActivity.getSignPointCount() > 0) {
|
||||||
|
try {
|
||||||
|
return Result.success("签到成功").addData(activityService.handleScanCodeSign(
|
||||||
|
courseId, SecurityUtil.getUserId(), lat, lng));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.error(99, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 主动扫码签到是用户自己打开扫一扫,扫二维码签到
|
// 主动扫码签到是用户自己打开扫一扫,扫二维码签到
|
||||||
int count = dao.count(TrainSignUpUserCourse.class, Cnd.where("courseId", "=", courseId).and("userId", "=", SecurityUtil.getUserId()));
|
int count = dao.count(TrainSignUpUserCourse.class, Cnd.where("courseId", "=", courseId).and("userId", "=", SecurityUtil.getUserId()));
|
||||||
@@ -113,6 +127,42 @@ public class TrainsignUpMineController {
|
|||||||
return Result.success("签到成功");
|
return Result.success("签到成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param courseId 扫码二维码中携带的课程点位ID
|
||||||
|
* @return Result.data为地图点位、签到进度、处理方式和动态表单配置
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@ApiOperation("查询签到扩展页面数据")
|
||||||
|
@SaCheckPermission("h5.trainSignUp.mine")
|
||||||
|
public Result getSignData(String courseId) {
|
||||||
|
try {
|
||||||
|
return Result.success().addData(activityService.getScanCodeSignData(courseId, SecurityUtil.getUserId()));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.error(99, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param courseId 当前扫码课程ID
|
||||||
|
* @param mobileColumnsValue JSON数组,每项包含字段编码和值
|
||||||
|
* @return Result,code为0表示保存成功,99表示业务校验未通过
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@ApiOperation("保存签到达标后的填写信息")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@SaCheckPermission("h5.trainSignUp.mine")
|
||||||
|
public Result saveDynamicForm(String courseId, String mobileColumnsValue) {
|
||||||
|
if (courseId == null || mobileColumnsValue == null) {
|
||||||
|
return Result.error(99, "请填写签到信息");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
activityService.saveScanCodeDynamicForm(courseId, SecurityUtil.getUserId(), mobileColumnsValue);
|
||||||
|
return Result.success("保存成功");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.error(99, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("被动扫码签到")
|
@ApiOperation("被动扫码签到")
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@@ -217,6 +267,13 @@ public class TrainsignUpMineController {
|
|||||||
@SaCheckPermission(value = {"trainSignUp.mine", "h5.trainSignUp.mine"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"trainSignUp.mine", "h5.trainSignUp.mine"}, mode = SaMode.OR)
|
||||||
public Result fetchCourse(String courseId) {
|
public Result fetchCourse(String courseId) {
|
||||||
TrainSignUpCourse course = dao.fetch(TrainSignUpCourse.class, courseId);
|
TrainSignUpCourse course = dao.fetch(TrainSignUpCourse.class, courseId);
|
||||||
return Result.success(course);
|
if (course == null) {
|
||||||
|
return Result.error(99, "未找到签到课程");
|
||||||
|
}
|
||||||
|
TrainSignUpActivity activity = dao.fetch(TrainSignUpActivity.class, course.getActivityId());
|
||||||
|
NutMap data = Lang.obj2nutmap(course);
|
||||||
|
data.put("signExtensionEnabled", activity != null && activity.getSignPointCount() != null
|
||||||
|
&& activity.getSignPointCount() > 0);
|
||||||
|
return Result.success(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,21 @@ public class TrainSignUpActivity extends BaseModel implements Serializable, SysH
|
|||||||
@ColDefine(type = ColType.MYSQL_JSON)
|
@ColDefine(type = ColType.MYSQL_JSON)
|
||||||
private List<String> helpUnits;
|
private List<String> helpUnits;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.INT)
|
||||||
|
@Comment("签到达标点位数,空或0表示未开启签到扩展")
|
||||||
|
private Integer signPointCount;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.INT)
|
||||||
|
@Comment("签到达标处理方式:1填写信息,2选择福利,3无")
|
||||||
|
private Integer signHandleType;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
@Comment("签到达标后关联的福利项目ID")
|
||||||
|
private String welfareProjectId;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Sys_home_activity covertToSysHomeActivity() {
|
public Sys_home_activity covertToSysHomeActivity() {
|
||||||
Sys_home_activity sysHomeActivity = new Sys_home_activity();
|
Sys_home_activity sysHomeActivity = new Sys_home_activity();
|
||||||
|
|||||||
+35
@@ -77,6 +77,12 @@ public interface TrainSignUpActivityService extends BaseService<TrainSignUpActiv
|
|||||||
*/
|
*/
|
||||||
void doSignUp(TrainSignUpUser trainSignUpUser) throws Exception;
|
void doSignUp(TrainSignUpUser trainSignUpUser) throws Exception;
|
||||||
|
|
||||||
|
/** activityId、courseId 分别为主活动和子活动 ID;返回该子活动可提醒的未报名人数。 */
|
||||||
|
int countReminderRecipients(String activityId, String courseId);
|
||||||
|
|
||||||
|
/** 根据主活动、子活动 ID 和管理员填写的正文提交钉钉提醒;返回本批次接收人数。 */
|
||||||
|
int submitSignupReminder(String activityId, String courseId, String content);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步插入每个报名成功人员的课程数据
|
* 异步插入每个报名成功人员的课程数据
|
||||||
*
|
*
|
||||||
@@ -128,4 +134,33 @@ public interface TrainSignUpActivityService extends BaseService<TrainSignUpActiv
|
|||||||
List<NutMap> qdInfoByUserId(String userId, String activityId);
|
List<NutMap> qdInfoByUserId(String userId, String activityId);
|
||||||
|
|
||||||
List<TrainSignUpCourse> filterCourseByHostUnion(List<TrainSignUpCourse> courseList);
|
List<TrainSignUpCourse> filterCourseByHostUnion(List<TrainSignUpCourse> courseList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询扩展签到页面数据。
|
||||||
|
*
|
||||||
|
* @param courseId 扫码进入的课程点位ID
|
||||||
|
* @param userId 当前登录用户ID
|
||||||
|
* @return 地图点位、签到进度、达标处理方式及动态表单字段
|
||||||
|
*/
|
||||||
|
NutMap getScanCodeSignData(String courseId, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成扩展点位签到。
|
||||||
|
*
|
||||||
|
* @param courseId 扫码进入的课程点位ID
|
||||||
|
* @param userId 当前登录用户ID
|
||||||
|
* @param latitude 浏览器定位得到的WGS84纬度
|
||||||
|
* @param longitude 浏览器定位得到的WGS84经度
|
||||||
|
* @return 最新签到进度及达标后的处理信息
|
||||||
|
*/
|
||||||
|
NutMap handleScanCodeSign(String courseId, String userId, Double latitude, Double longitude);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存签到达标后填写的动态表单。
|
||||||
|
*
|
||||||
|
* @param courseId 当前扫码课程ID
|
||||||
|
* @param userId 当前登录用户ID
|
||||||
|
* @param mobileColumnsValue JSON数组,每项包含columnCode和columnValue
|
||||||
|
*/
|
||||||
|
void saveScanCodeDynamicForm(String courseId, String userId, String mobileColumnsValue);
|
||||||
}
|
}
|
||||||
|
|||||||
+341
@@ -1,6 +1,9 @@
|
|||||||
package com.budwk.app.zhgh.activity.trainSignUp.service.impl;
|
package com.budwk.app.zhgh.activity.trainSignUp.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.core.thread.ThreadUtil;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import com.budwk.app.base.sms.SmsService;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
@@ -14,6 +17,8 @@ import com.budwk.app.zhgh.activity.family.service.FamilyActivityStatisticsServic
|
|||||||
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
|
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
||||||
|
import com.budwk.app.zhgh.welfare.model.WelfareProject;
|
||||||
|
import com.budwk.app.zhgh.welfare.service.WelfareListService;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.aop.interceptor.async.Async;
|
import org.nutz.aop.interceptor.async.Async;
|
||||||
@@ -27,6 +32,7 @@ import org.nutz.dao.util.cri.Static;
|
|||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.json.Json;
|
||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
@@ -45,6 +51,84 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private TrainSignUpActivityStatisticsService statisticsService;
|
private TrainSignUpActivityStatisticsService statisticsService;
|
||||||
|
@Inject
|
||||||
|
private WelfareListService welfareListService;
|
||||||
|
@Inject
|
||||||
|
private SmsService smsService;
|
||||||
|
|
||||||
|
/** 按主活动及子活动 ID 筛选可提醒人员,返回去重后的工号数量。 */
|
||||||
|
@Override
|
||||||
|
public int countReminderRecipients(String activityId, String courseId) {
|
||||||
|
return listReminderRecipients(activityId, courseId).size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按主活动、子活动和正文提交后台钉钉提醒,返回本次接收人数,不代表送达数。 */
|
||||||
|
@Override
|
||||||
|
public int submitSignupReminder(String activityId, String courseId, String content) {
|
||||||
|
if (StrUtil.isBlank(content)) {
|
||||||
|
throw new IllegalArgumentException("请输入提醒内容");
|
||||||
|
}
|
||||||
|
// 预览人数可能已变化,发送前重新筛选,不能信任前端提交的接收人。
|
||||||
|
List<String> recipients = listReminderRecipients(activityId, courseId);
|
||||||
|
if (recipients.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("该子活动没有符合条件的未报名人员");
|
||||||
|
}
|
||||||
|
String sender = SecurityUtil.getUserLoginname();
|
||||||
|
if (StrUtil.isBlank(sender)) {
|
||||||
|
throw new IllegalArgumentException("无法获取发送人工号");
|
||||||
|
}
|
||||||
|
String url = "/platform/trainSignUp/apply/h5?id=" + activityId;
|
||||||
|
String message = content.trim();
|
||||||
|
ThreadUtil.execute(() -> smsService.massSendTextCard(recipients, "品牌活动报名提醒", message, null, url, sender));
|
||||||
|
return recipients.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据主活动范围和子活动报名记录返回工号列表;其他子活动的报名不影响本次筛选。
|
||||||
|
* 停用、过期或子活动归属不匹配时拒绝发送,空人员范围返回空列表。
|
||||||
|
*/
|
||||||
|
private List<String> listReminderRecipients(String activityId, String courseId) {
|
||||||
|
if (StrUtil.isBlank(activityId) || StrUtil.isBlank(courseId)) {
|
||||||
|
throw new IllegalArgumentException("请选择子活动");
|
||||||
|
}
|
||||||
|
TrainSignUpActivity activity = dao().fetch(TrainSignUpActivity.class, activityId);
|
||||||
|
TrainSignUpCourse course = dao().fetch(TrainSignUpCourse.class, courseId);
|
||||||
|
if (activity == null || course == null || !activityId.equals(course.getActivityId())) {
|
||||||
|
throw new IllegalArgumentException("当前活动或子活动不存在");
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.isDisabled() || activity.getActivitySignUpEndTime() == null
|
||||||
|
|| !now.before(activity.getActivitySignUpEndTime())) {
|
||||||
|
throw new IllegalArgumentException("活动已停用或报名已截止,无法发送提醒");
|
||||||
|
}
|
||||||
|
if (activity.getActivityGroupId() == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
// 与子活动对内开放规则一致:开放时间前只提醒承办工会的人员。
|
||||||
|
boolean hostOnly = Boolean.FALSE.equals(course.getOpenOtherUnion())
|
||||||
|
&& StrUtil.isNotBlank(course.getInterTime())
|
||||||
|
&& DateUtil.compare(now, DateUtil.parse(course.getInterTime()), "yyyy-MM-dd HH:mm") < 0;
|
||||||
|
Set<String> allowedUnions = Lang.isEmpty(course.getUnionLimit()) ? Set.of()
|
||||||
|
: course.getUnionLimit().stream().map(v -> v.getString("id")).filter(StrUtil::isNotBlank).collect(Collectors.toSet());
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT DISTINCT u.loginname, u.unionId
|
||||||
|
FROM activity_user_scope s
|
||||||
|
JOIN vw_user u ON u.id = s.userId
|
||||||
|
WHERE s.groupId = @groupId
|
||||||
|
AND u.loginname IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM train_sign_up_user r
|
||||||
|
WHERE r.courseId = @courseId AND r.userId = s.userId
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
sql.setParam("groupId", activity.getActivityGroupId());
|
||||||
|
sql.setParam("courseId", courseId);
|
||||||
|
return listMap(sql).stream()
|
||||||
|
.filter(row -> !hostOnly || Objects.equals(course.getHostUnionId(), row.getString("unionId")))
|
||||||
|
.filter(row -> Lang.isEmpty(course.getUnionLimit()) || allowedUnions.contains(row.getString("unionId")))
|
||||||
|
.map(row -> row.getString("loginname"))
|
||||||
|
.filter(StrUtil::isNotBlank).map(String::trim).distinct().toList();
|
||||||
|
}
|
||||||
|
|
||||||
public TrainSignUpActivityServiceImpl(Dao dao) {
|
public TrainSignUpActivityServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
@@ -53,6 +137,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void add(TrainSignUpActivity activity, TrainSignUpCourse course) {
|
public void add(TrainSignUpActivity activity, TrainSignUpCourse course) {
|
||||||
|
validateSignExtension(activity);
|
||||||
|
|
||||||
dao().insert(activity);
|
dao().insert(activity);
|
||||||
|
|
||||||
@@ -78,6 +163,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void edit(TrainSignUpActivity activity) {
|
public void edit(TrainSignUpActivity activity) {
|
||||||
|
validateSignExtension(activity);
|
||||||
// 1. 更新主活动
|
// 1. 更新主活动
|
||||||
update(activity);
|
update(activity);
|
||||||
// 2. 处理类型限制
|
// 2. 处理类型限制
|
||||||
@@ -524,6 +610,261 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
return listMap(sql);
|
return listMap(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NutMap getScanCodeSignData(String courseId, String userId) {
|
||||||
|
TrainSignUpCourse course = requireScanCodeCourse(courseId, userId);
|
||||||
|
TrainSignUpActivity activity = dao().fetch(TrainSignUpActivity.class, course.getActivityId());
|
||||||
|
return buildScanCodeData(activity, course, userId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public NutMap handleScanCodeSign(String courseId, String userId, Double latitude, Double longitude) {
|
||||||
|
lockScanCodeUser(userId);
|
||||||
|
TrainSignUpCourse course = requireScanCodeCourse(courseId, userId);
|
||||||
|
TrainSignUpActivity activity = dao().fetch(TrainSignUpActivity.class, course.getActivityId());
|
||||||
|
requireScanCodeLocation(course.getTransPosition(), latitude, longitude, effectiveRadius(course));
|
||||||
|
|
||||||
|
List<TrainSignUpUserCourse> records = dao().query(TrainSignUpUserCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activity.getId()).and("courseId", "=", courseId)
|
||||||
|
.and("userId", "=", userId).asc("courseStartTime"));
|
||||||
|
if (records.stream().anyMatch(TrainSignUpUserCourse::isAttend)) {
|
||||||
|
return buildScanCodeData(activity, course, userId, true)
|
||||||
|
.addv("message", "此点位,您已经签到");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
int signUnit = course.getSignUnit() == null ? 0 : course.getSignUnit();
|
||||||
|
List<TrainSignUpUserCourse> availableRecords = records.stream().filter(item -> item.getCourseStartTime() != null
|
||||||
|
&& item.getCourseEndTime() != null
|
||||||
|
&& !now.before(cn.hutool.core.date.DateUtil.offsetMinute(item.getCourseStartTime(), -signUnit))
|
||||||
|
&& !now.after(cn.hutool.core.date.DateUtil.offsetMinute(item.getCourseEndTime(), signUnit)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (availableRecords.size() != 1) {
|
||||||
|
throw new IllegalArgumentException(availableRecords.isEmpty() ? "当前不在签到时间范围内" : "当前点位存在多个可签到课次,请联系管理员");
|
||||||
|
}
|
||||||
|
TrainSignUpUserCourse userCourse = availableRecords.get(0);
|
||||||
|
userCourse.setAttend(true);
|
||||||
|
userCourse.setAttendTime(now);
|
||||||
|
dao().update(userCourse);
|
||||||
|
return buildScanCodeData(activity, course, userId, true).addv("message", "签到成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void saveScanCodeDynamicForm(String courseId, String userId, String mobileColumnsValue) {
|
||||||
|
lockScanCodeUser(userId);
|
||||||
|
TrainSignUpCourse course = requireScanCodeCourse(courseId, userId);
|
||||||
|
TrainSignUpActivity activity = dao().fetch(TrainSignUpActivity.class, course.getActivityId());
|
||||||
|
if (!Integer.valueOf(1).equals(activity.getSignHandleType())) {
|
||||||
|
throw new IllegalArgumentException("当前活动无需填写信息");
|
||||||
|
}
|
||||||
|
if (!buildScanCodeData(activity, course, userId, false).getBoolean("isReachTarget", false)) {
|
||||||
|
throw new IllegalArgumentException("当前活动尚未达到签到点位数");
|
||||||
|
}
|
||||||
|
List<NutMap> inputData;
|
||||||
|
try {
|
||||||
|
inputData = Json.fromJsonAsList(NutMap.class, mobileColumnsValue);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("填写信息格式不正确");
|
||||||
|
}
|
||||||
|
if (inputData == null) {
|
||||||
|
throw new IllegalArgumentException("请填写当前签到信息");
|
||||||
|
}
|
||||||
|
Map<String, NutMap> inputMap = new LinkedHashMap<>();
|
||||||
|
for (NutMap item : inputData) {
|
||||||
|
String code = item == null ? null : item.getString("columnCode");
|
||||||
|
if (StrUtil.isBlank(code) || inputMap.put(code, item) != null) {
|
||||||
|
throw new IllegalArgumentException("填写信息包含无效或重复字段");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<TrainMobileSignColumn> columns = getScanCodeColumns(course);
|
||||||
|
List<NutMap> normalized = new ArrayList<>();
|
||||||
|
for (TrainMobileSignColumn column : columns) {
|
||||||
|
NutMap input = inputMap.get(column.getColumnCode());
|
||||||
|
Object value = input == null ? null : input.get("columnValue");
|
||||||
|
if (Boolean.TRUE.equals(column.getIsRequired()) && isEmptyDynamicValue(value)) {
|
||||||
|
throw new IllegalArgumentException("请填写" + column.getColumnName());
|
||||||
|
}
|
||||||
|
normalized.add(NutMap.NEW().addv("columnCode", column.getColumnCode())
|
||||||
|
.addv("columnName", column.getColumnName()).addv("columnFormType", column.getColumnFormType())
|
||||||
|
.addv("columnValue", value));
|
||||||
|
}
|
||||||
|
List<TrainSignUpUser> signUsers = dao().query(TrainSignUpUser.class,
|
||||||
|
Cnd.where("activityId", "=", activity.getId()).and("userId", "=", userId));
|
||||||
|
signUsers.forEach(item -> item.setMobileColumnsValue(normalized));
|
||||||
|
dao().update(signUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存活动前统一校验签到扩展,未开启时清理无效配置,避免历史活动被新逻辑影响。 */
|
||||||
|
private void validateSignExtension(TrainSignUpActivity activity) {
|
||||||
|
if (activity.getSignPointCount() == null || activity.getSignPointCount() <= 0) {
|
||||||
|
activity.setSignPointCount(null);
|
||||||
|
activity.setSignHandleType(3);
|
||||||
|
activity.setWelfareProjectId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<TrainSignUpCourse> courses = activity.getCourseList();
|
||||||
|
if (Lang.isEmpty(courses) || activity.getSignPointCount() > courses.size()) {
|
||||||
|
throw new IllegalArgumentException("签到达标点位数不能超过活动课程数量");
|
||||||
|
}
|
||||||
|
for (TrainSignUpCourse item : courses) {
|
||||||
|
if (!item.isMobileSign() || !Integer.valueOf(1).equals(item.getSignType())) {
|
||||||
|
throw new IllegalArgumentException("开启签到扩展后,所有课程必须启用扫码签到");
|
||||||
|
}
|
||||||
|
if (!validCoordinates(item.getCourseLocationCoordinates()) || !validCoordinates(item.getTransPosition())) {
|
||||||
|
throw new IllegalArgumentException("课程“" + item.getCourseName() + "”未配置有效签到坐标");
|
||||||
|
}
|
||||||
|
if (item.getRadius() != null && item.getRadius() <= 0) {
|
||||||
|
throw new IllegalArgumentException("课程“" + item.getCourseName() + "”签到半径必须大于0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Integer handleType = activity.getSignHandleType() == null ? 3 : activity.getSignHandleType();
|
||||||
|
if (!List.of(1, 2, 3).contains(handleType)) {
|
||||||
|
throw new IllegalArgumentException("签到达标处理方式无效");
|
||||||
|
}
|
||||||
|
activity.setSignHandleType(handleType);
|
||||||
|
if (handleType == 1 && (courses.stream().anyMatch(item -> StrUtil.isBlank(item.getCourseType()))
|
||||||
|
|| courses.stream().map(TrainSignUpCourse::getCourseType).distinct().count() != 1)) {
|
||||||
|
throw new IllegalArgumentException("达标后填写信息要求所有课程使用同一种课程类型");
|
||||||
|
}
|
||||||
|
if (handleType == 2) {
|
||||||
|
WelfareProject project = StrUtil.isBlank(activity.getWelfareProjectId()) ? null
|
||||||
|
: dao().fetch(WelfareProject.class, activity.getWelfareProjectId());
|
||||||
|
if (project == null) {
|
||||||
|
throw new IllegalArgumentException("请选择有效的福利项目");
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(project.getIsDisabled())
|
||||||
|
|| project.getChoiceTimeEnd() != null && project.getChoiceTimeEnd().before(new Date())) {
|
||||||
|
throw new IllegalArgumentException("所选福利项目已禁用或已超过选择截止时间");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activity.setWelfareProjectId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验扩展签到入口,不改变当前项目“必须先报名才能签到”的原有规则。 */
|
||||||
|
private TrainSignUpCourse requireScanCodeCourse(String courseId, String userId) {
|
||||||
|
TrainSignUpCourse course = StrUtil.isBlank(courseId) ? null : dao().fetch(TrainSignUpCourse.class, courseId);
|
||||||
|
if (course == null || !course.isMobileSign() || !Integer.valueOf(1).equals(course.getSignType())) {
|
||||||
|
throw new IllegalArgumentException("当前课程未开启扫码签到");
|
||||||
|
}
|
||||||
|
TrainSignUpActivity activity = dao().fetch(TrainSignUpActivity.class, course.getActivityId());
|
||||||
|
if (activity == null || activity.getSignPointCount() == null || activity.getSignPointCount() <= 0) {
|
||||||
|
throw new IllegalArgumentException("当前活动未开启签到扩展");
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.isDisabled() || activity.getActivityStartTime() == null || activity.getActivityEndTime() == null
|
||||||
|
|| now.before(activity.getActivityStartTime()) || now.after(activity.getActivityEndTime())) {
|
||||||
|
throw new IllegalArgumentException("当前活动未开放或不在活动时间范围内");
|
||||||
|
}
|
||||||
|
List<TrainSignUpUser> signUsers = dao().query(TrainSignUpUser.class,
|
||||||
|
Cnd.where("activityId", "=", activity.getId()).and("courseId", "=", courseId).and("userId", "=", userId));
|
||||||
|
if (signUsers.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("未查询到您的报名记录");
|
||||||
|
}
|
||||||
|
if (signUsers.stream().noneMatch(item -> Integer.valueOf(1).equals(item.getState()) || Integer.valueOf(3).equals(item.getState()))) {
|
||||||
|
throw new IllegalArgumentException("当前报名状态不能签到");
|
||||||
|
}
|
||||||
|
return course;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组装当前点位、全部点位、签到进度及达标处理状态。 */
|
||||||
|
private NutMap buildScanCodeData(TrainSignUpActivity activity, TrainSignUpCourse course, String userId, boolean allowHandle) {
|
||||||
|
List<TrainSignUpUserCourse> attended = dao().query(TrainSignUpUserCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activity.getId()).and("userId", "=", userId).and("isAttend", "=", true));
|
||||||
|
long signCount = attended.stream().map(TrainSignUpUserCourse::getCourseId).filter(StrUtil::isNotBlank).distinct().count();
|
||||||
|
boolean reached = signCount >= activity.getSignPointCount();
|
||||||
|
List<TrainSignUpCourse> courses = dao().query(TrainSignUpCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activity.getId()).asc("orderNum"));
|
||||||
|
List<NutMap> signPoints = courses.stream().map(item -> NutMap.NEW().addv("courseId", item.getId())
|
||||||
|
.addv("courseName", item.getCourseName()).addv("courseLocation", item.getCourseLocation())
|
||||||
|
.addv("signed", attended.stream().anyMatch(record -> Objects.equals(record.getCourseId(), item.getId()))))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<String, Object> savedValues = new HashMap<>();
|
||||||
|
List<TrainSignUpUser> signUsers = dao().query(TrainSignUpUser.class,
|
||||||
|
Cnd.where("activityId", "=", activity.getId()).and("userId", "=", userId));
|
||||||
|
signUsers.stream().map(TrainSignUpUser::getMobileColumnsValue).filter(Lang::isNotEmpty).findFirst()
|
||||||
|
.ifPresent(values -> values.forEach(value -> savedValues.put(value.getString("columnCode"), value.get("columnValue"))));
|
||||||
|
List<NutMap> dynamicColumns = getScanCodeColumns(course).stream().map(item -> {
|
||||||
|
NutMap map = Lang.obj2nutmap(item);
|
||||||
|
map.put("columnValue", savedValues.containsKey(item.getColumnCode()) ? savedValues.get(item.getColumnCode())
|
||||||
|
: "JSON".equals(item.getColumnType()) ? new ArrayList<>() : "");
|
||||||
|
return map;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
NutMap data = NutMap.NEW().addv("activityName", activity.getActivityName()).addv("courseName", course.getCourseName())
|
||||||
|
.addv("courseLocation", course.getCourseLocation()).addv("courseLocationCoordinates", course.getCourseLocationCoordinates())
|
||||||
|
.addv("signRadius", effectiveRadius(course)).addv("currentPointSigned", attended.stream()
|
||||||
|
.anyMatch(item -> Objects.equals(item.getCourseId(), course.getId())))
|
||||||
|
.addv("signCount", signCount).addv("signPointCount", activity.getSignPointCount())
|
||||||
|
.addv("signPoints", signPoints).addv("isReachTarget", reached)
|
||||||
|
.addv("signHandleType", activity.getSignHandleType()).addv("dynamicColumnsData", dynamicColumns)
|
||||||
|
.addv("needDynamicForm", reached && Integer.valueOf(1).equals(activity.getSignHandleType()) && !dynamicColumns.isEmpty())
|
||||||
|
.addv("needWelfareSelect", false).addv("welfareJumpUrl", "")
|
||||||
|
.addv("handleMsg", reached ? "当前活动已达标" : "请继续完成其他点位签到");
|
||||||
|
if (reached && Integer.valueOf(2).equals(activity.getSignHandleType())) {
|
||||||
|
WelfareProject welfareProject = dao().fetch(WelfareProject.class, activity.getWelfareProjectId());
|
||||||
|
if (welfareProject == null || Boolean.TRUE.equals(welfareProject.getIsDisabled())
|
||||||
|
|| welfareProject.getChoiceTimeEnd() != null && welfareProject.getChoiceTimeEnd().before(new Date())) {
|
||||||
|
return data.setv("handleMsg", "关联福利项目已结束,请联系管理员");
|
||||||
|
}
|
||||||
|
if (allowHandle) {
|
||||||
|
welfareListService.ensureWelfareListUser(userId, activity.getWelfareProjectId());
|
||||||
|
}
|
||||||
|
data.setv("needWelfareSelect", true)
|
||||||
|
.setv("welfareJumpUrl", "/platform/h5/welfare/userSelect/index?id=" + activity.getWelfareProjectId())
|
||||||
|
.setv("handleMsg", "签到已达标,请选择福利");
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TrainMobileSignColumn> getScanCodeColumns(TrainSignUpCourse course) {
|
||||||
|
return StrUtil.isBlank(course.getCourseType()) ? new ArrayList<>() : dao().query(TrainMobileSignColumn.class,
|
||||||
|
Cnd.where("typeId", "=", course.getCourseType()).asc("columnIndex"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 锁定当前登录用户,避免同一用户并发扫码造成重复达标处理。 */
|
||||||
|
private void lockScanCodeUser(String userId) {
|
||||||
|
if (StrUtil.isBlank(userId)) {
|
||||||
|
throw new IllegalArgumentException("未获取到当前登录用户");
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("SELECT id FROM sys_user WHERE id = @userId FOR UPDATE").setParam("userId", userId);
|
||||||
|
sql.setCallback(Sqls.callback.map());
|
||||||
|
dao().execute(sql);
|
||||||
|
NutMap user = sql.getObject(NutMap.class);
|
||||||
|
if (user == null || user.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("未找到当前用户信息");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int effectiveRadius(TrainSignUpCourse course) {
|
||||||
|
return course.getRadius() == null ? 100 : course.getRadius();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validCoordinates(List<Double> coordinates) {
|
||||||
|
return coordinates != null && coordinates.size() == 2 && coordinates.get(0) != null && coordinates.get(1) != null
|
||||||
|
&& Double.isFinite(coordinates.get(0)) && Double.isFinite(coordinates.get(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前定位与transPosition均为WGS84坐标,沿用原签到距离计算口径。 */
|
||||||
|
private void requireScanCodeLocation(List<Double> coordinates, Double latitude, Double longitude, int radius) {
|
||||||
|
if (!validCoordinates(coordinates)) {
|
||||||
|
throw new IllegalArgumentException("当前点位未配置有效签到坐标");
|
||||||
|
}
|
||||||
|
if (latitude == null || longitude == null || !Double.isFinite(latitude) || !Double.isFinite(longitude)) {
|
||||||
|
throw new IllegalArgumentException("请先获取当前定位后再签到");
|
||||||
|
}
|
||||||
|
double distance = com.budwk.app.base.utils.DistanceUtil.getDistance(coordinates.get(0), coordinates.get(1), latitude, longitude);
|
||||||
|
if (distance > radius) {
|
||||||
|
throw new IllegalArgumentException("签到失败,您距离签到点还有" + String.format("%.1f", distance - radius) + "米");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEmptyDynamicValue(Object value) {
|
||||||
|
return value == null || value instanceof String && StrUtil.isBlank((String) value)
|
||||||
|
|| value instanceof Collection && ((Collection<?>) value).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<TrainSignUpCourse> filterCourseByHostUnion(List<TrainSignUpCourse> courseList) {
|
public List<TrainSignUpCourse> filterCourseByHostUnion(List<TrainSignUpCourse> courseList) {
|
||||||
if (Lang.isEmpty(courseList)) {
|
if (Lang.isEmpty(courseList)) {
|
||||||
|
|||||||
+22
-10
@@ -17,6 +17,7 @@ import org.nutz.dao.Dao;
|
|||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
@@ -59,18 +60,17 @@ public class QsvActivityController {
|
|||||||
//保存问卷基础信息
|
//保存问卷基础信息
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.activity")
|
@SaCheckPermission("qsv.activity")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@ApiOperation("保存问卷基础信息")
|
@ApiOperation("保存问卷基础信息")
|
||||||
@SLog(type = "qsv.activity", tag = "保存问卷基础信息", msg = "保存问卷基础信息")
|
@SLog(type = "qsv.activity", tag = "保存问卷基础信息", msg = "保存问卷基础信息")
|
||||||
public Result save(QsvActivity qsvActivity) {
|
public Result save(QsvActivity qsvActivity, @Param("forceSave") Boolean forceSave) {
|
||||||
if (qsvActivity.getCategory().equals("QUIZ")) {
|
NutMap confirmResult = qsvActivityService.checkAnswerModeChangeConfirm(qsvActivity);
|
||||||
if (qsvActivity.getMode().equals("SCHEDULED")) {
|
// Mode changes with existing answer records must be returned as a normal response, so the page can show a confirm dialog.
|
||||||
qsvActivity.setRepeatMode("DAILY");
|
if (!Boolean.TRUE.equals(forceSave) && Boolean.TRUE.equals(confirmResult.get("needConfirm"))) {
|
||||||
} else if (qsvActivity.getMode().equals("REGULAR")) {
|
return Result.success(confirmResult.getString("message"), confirmResult);
|
||||||
qsvActivity.setRepeatMode("TOTAL");
|
|
||||||
}
|
}
|
||||||
}
|
QsvActivity activity = qsvActivityService.saveActivity(qsvActivity, forceSave);
|
||||||
dao.insertOrUpdate(qsvActivity);
|
return Result.success(activity);
|
||||||
return Result.success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@@ -83,13 +83,25 @@ public class QsvActivityController {
|
|||||||
// 删除问卷
|
// 删除问卷
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.activity")
|
@SaCheckPermission("qsv.activity")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@ApiOperation("删除问卷")
|
@ApiOperation("删除问卷")
|
||||||
@SLog(type = "qsv.activity", tag = "删除问卷", msg = "删除问卷")
|
@SLog(type = "qsv.activity", tag = "删除问卷", msg = "删除问卷")
|
||||||
public Result delete(@Valid String id) {
|
public Result delete(@Valid String id) {
|
||||||
dao.delete(QsvActivity.class, id);
|
qsvActivityService.deleteActivity(id);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新活动开启状态
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.activity")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@ApiOperation("更新活动开启状态")
|
||||||
|
@SLog(type = "qsv.activity", tag = "更新活动开启状态", msg = "更新活动开启状态")
|
||||||
|
public Result updateEnabled(@Valid String id, Boolean enabled) {
|
||||||
|
qsvActivityService.updateEnabled(id, enabled);
|
||||||
|
return Result.success(enabled ? "开启成功" : "关闭成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 保存问卷题目
|
// 保存问卷题目
|
||||||
@At
|
@At
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import com.budwk.app.zhgh.dayofficework.qsv.models.QsvBank;
|
|||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvBankService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvBankService;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
@@ -51,6 +53,7 @@ public class QsvBankController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.bank")
|
@SaCheckPermission("qsv.bank")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@ApiOperation("保存题库")
|
@ApiOperation("保存题库")
|
||||||
public Result save(@Param("qsvBank") QsvBank qsvBank) {
|
public Result save(@Param("qsvBank") QsvBank qsvBank) {
|
||||||
dao.insert(qsvBank);
|
dao.insert(qsvBank);
|
||||||
@@ -59,6 +62,7 @@ public class QsvBankController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.bank")
|
@SaCheckPermission("qsv.bank")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@ApiOperation("修改题库")
|
@ApiOperation("修改题库")
|
||||||
public Result update(@Param("qsvBank") QsvBank qsvBank) {
|
public Result update(@Param("qsvBank") QsvBank qsvBank) {
|
||||||
dao.updateIgnoreNull(qsvBank);
|
dao.updateIgnoreNull(qsvBank);
|
||||||
@@ -75,6 +79,7 @@ public class QsvBankController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.bank")
|
@SaCheckPermission("qsv.bank")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@ApiOperation("删除题库")
|
@ApiOperation("删除题库")
|
||||||
public Result delete(@Valid String id) {
|
public Result delete(@Valid String id) {
|
||||||
dao.delete(QsvBank.class, id);
|
dao.delete(QsvBank.class, id);
|
||||||
@@ -83,6 +88,7 @@ public class QsvBankController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.bank")
|
@SaCheckPermission("qsv.bank")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@ApiOperation("保存题库题目")
|
@ApiOperation("保存题库题目")
|
||||||
public Result saveSubjects(@Valid String id, @Param("subjects") QsvSubject[] subjects) {
|
public Result saveSubjects(@Valid String id, @Param("subjects") QsvSubject[] subjects) {
|
||||||
QsvBank qsvBank = dao.fetch(QsvBank.class, id);
|
QsvBank qsvBank = dao.fetch(QsvBank.class, id);
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.qsv.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/qsv")
|
||||||
|
@Ok("json:full")
|
||||||
|
@ApiOperation("问卷调查新建")
|
||||||
|
/**
|
||||||
|
* 问卷新建页面控制器,负责承载 PC 端问卷创建向导入口。
|
||||||
|
*/
|
||||||
|
public class QsvNewController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开问卷新建页面,页面内继续完成基础信息、题目与发布配置。
|
||||||
|
*/
|
||||||
|
@At("/new")
|
||||||
|
@Ok("beetl:/platform/zhgh/dayofficework/qsv/activity/new.html")
|
||||||
|
@SaCheckPermission("qsv.new")
|
||||||
|
public void newPage() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+611
@@ -0,0 +1,611 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.qsv.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.lang.Dict;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import com.budwk.app.base.page.Pagination;
|
||||||
|
import com.budwk.app.base.param.PageForm;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.dto.QsvCheckAnswerResult;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvQuizService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
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;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/qsv/online")
|
||||||
|
@Ok("json:full")
|
||||||
|
@ApiOperation("在线答题")
|
||||||
|
/**
|
||||||
|
* PC 端在线答题控制器,负责答题/投票任务列表、题目读取、提交和投票统计。
|
||||||
|
*/
|
||||||
|
public class QsvOnlineController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Dao dao;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
|
||||||
|
@Inject
|
||||||
|
private QsvQuizService qsvQuizService;
|
||||||
|
|
||||||
|
@At("")
|
||||||
|
@Ok("beetl:/platform/zhgh/dayofficework/qsv/online/index.html")
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
/**
|
||||||
|
* 打开 PC 端在线答题页面。
|
||||||
|
*/
|
||||||
|
public void index() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询当前用户可参与的问卷活动,并根据是否已答过滤列表。
|
||||||
|
*
|
||||||
|
* @param pageForm 分页参数
|
||||||
|
* @param year 活动年度
|
||||||
|
* @param title 活动标题关键字
|
||||||
|
* @param isAnswered 是否只查询已答记录
|
||||||
|
* @return 当前用户可见的在线答题分页数据
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
@ApiOperation("在线答题任务分页")
|
||||||
|
public Result pageData(@Valid PageForm pageForm, Integer year, String title, Boolean isAnswered) {
|
||||||
|
// 先按用户参与范围过滤活动,避免用户看到无权限参与的问卷。
|
||||||
|
List<Integer> groupIds = dao.query(ActivityUserScope.class, Cnd.where("userId", "=", SecurityUtil.getUserId()))
|
||||||
|
.stream().map(ActivityUserScope::getGroupId).collect(Collectors.toList());
|
||||||
|
if (groupIds.isEmpty()) {
|
||||||
|
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Cnd cnd = Cnd.where("enabled", "=", true);
|
||||||
|
cnd.and("groupId", "in", groupIds);
|
||||||
|
cnd.andEX("YEAR(startTime)", "=", year);
|
||||||
|
cnd.and(Cnd.likeEX("title", title));
|
||||||
|
cnd.desc("startTime");
|
||||||
|
|
||||||
|
int totalCount = dao.count(QsvActivity.class, cnd);
|
||||||
|
if (totalCount == 0) {
|
||||||
|
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<>()));
|
||||||
|
}
|
||||||
|
List<QsvActivity> activities = dao.query(QsvActivity.class, cnd);
|
||||||
|
List<String> activityIds = activities.stream().map(QsvActivity::getId).collect(Collectors.toList());
|
||||||
|
// 预加载用户答题记录,后续构建列表状态时避免循环查询数据库。
|
||||||
|
Map<String, List<QsvUserAnswerRecord>> recordGroup = dao.query(QsvUserAnswerRecord.class, Cnd.where("userId", "=", SecurityUtil.getUserId())
|
||||||
|
.and("activityId", "in", activityIds))
|
||||||
|
.stream().collect(Collectors.groupingBy(QsvUserAnswerRecord::getActivityId));
|
||||||
|
List<NutMap> rows = activities.stream()
|
||||||
|
.map((activity) -> buildOnlineRow(activity, recordGroup.getOrDefault(activity.getId(), new ArrayList<>())))
|
||||||
|
.filter((row) -> {
|
||||||
|
if (Boolean.TRUE.equals(isAnswered)) {
|
||||||
|
return row.getBoolean("isAnswered");
|
||||||
|
}
|
||||||
|
return !row.getBoolean("isAnswered") || row.getBoolean("canAnswer");
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
int filteredTotal = rows.size();
|
||||||
|
int fromIndex = Math.min((pageForm.getPageNumber() - 1) * pageForm.getPageSize(), filteredTotal);
|
||||||
|
int toIndex = Math.min(fromIndex + pageForm.getPageSize(), filteredTotal);
|
||||||
|
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), filteredTotal, rows.subList(fromIndex, toIndex)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@ApiOperation("在线答题题目")
|
||||||
|
/**
|
||||||
|
* 查询在线答题题目,并在用户首次进入时生成答题记录。
|
||||||
|
*
|
||||||
|
* @param activityId 活动 ID
|
||||||
|
* @param viewOnly 是否仅查看已完成答题
|
||||||
|
* @return 活动、题目和答题记录信息
|
||||||
|
*/
|
||||||
|
public Result subjects(@Valid String activityId, Boolean viewOnly) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
if (activity == null) {
|
||||||
|
return Result.error("调查不存在");
|
||||||
|
}
|
||||||
|
QsvUserAnswerRecord answerRecord = findCurrentAnswerRecord(activityId, Boolean.TRUE.equals(viewOnly));
|
||||||
|
if (Boolean.TRUE.equals(viewOnly) && answerRecord == null) {
|
||||||
|
return Result.error("未找到已完成的答题记录");
|
||||||
|
}
|
||||||
|
boolean finishedView = Boolean.TRUE.equals(viewOnly) && answerRecord != null && Boolean.TRUE.equals(answerRecord.getIsFinish());
|
||||||
|
if (!finishedView) {
|
||||||
|
Result activityStatusResult = checkActivityStatus(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Result scopeResult = checkUserScope(activity);
|
||||||
|
if (scopeResult != null) {
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
answerRecord = prepareAnswerRecord(activity, answerRecord);
|
||||||
|
List<QsvSubject> activitySubjects = queryActivitySubjectsForSync(activity);
|
||||||
|
// 后台调整题目后,同步未完成记录的题目范围,保证用户继续作答时题目完整。
|
||||||
|
syncAnswerRecordSubjects(answerRecord, activity, activitySubjects);
|
||||||
|
|
||||||
|
List<QsvSubject> subjects = querySubjectsByRecordOrder(answerRecord.getSubjectIds());
|
||||||
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
|
subjects.forEach((subject) -> {
|
||||||
|
if (subject.getUserSelectOptionIds() == null) {
|
||||||
|
subject.setUserSelectOptionIds(new ArrayList<>());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Result.success(NutMap.NEW()
|
||||||
|
.addv("activity", activity)
|
||||||
|
.addv("subjects", subjects)
|
||||||
|
.addv("answerRecordId", answerRecord.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
@ApiOperation("在线答题记录")
|
||||||
|
/**
|
||||||
|
* 查询单条答题记录,用于恢复用户已填内容。
|
||||||
|
*
|
||||||
|
* @param answerRecordId 答题记录 ID
|
||||||
|
* @return 答题记录
|
||||||
|
*/
|
||||||
|
public Result answerRecord(@Valid String answerRecordId) {
|
||||||
|
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
|
||||||
|
return Result.success(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
@ApiOperation("在线投票统计")
|
||||||
|
/**
|
||||||
|
* 查询投票活动的选项票数、占比和排名。
|
||||||
|
*
|
||||||
|
* @param activityId 投票活动 ID
|
||||||
|
* @return 投票统计数据
|
||||||
|
*/
|
||||||
|
public Result voteStats(@Valid String activityId) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
if (activity == null || !"VOTE".equals(activity.getCategory())) {
|
||||||
|
return Result.error("投票不存在");
|
||||||
|
}
|
||||||
|
Result scopeResult = checkUserScope(activity);
|
||||||
|
if (scopeResult != null) {
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
return Result.success(buildVoteStats(activityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@ApiOperation("提交在线答题")
|
||||||
|
/**
|
||||||
|
* 提交在线答题或投票结果,并写入用户答题记录。
|
||||||
|
*
|
||||||
|
* @param qsvAnswerParam 用户提交的题目答案
|
||||||
|
* @return 提交结果
|
||||||
|
*/
|
||||||
|
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId());
|
||||||
|
Result activityStatusResult = checkActivityStatus(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
Result scopeResult = checkUserScope(activity);
|
||||||
|
if (scopeResult != null) {
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
|
||||||
|
if (answerRecord == null || !SecurityUtil.getUserId().equals(answerRecord.getUserId())) {
|
||||||
|
return Result.error("答题记录不存在");
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(answerRecord.getIsFinish())) {
|
||||||
|
return Result.error("您已完成该调查");
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
extJson = new JSONObject();
|
||||||
|
}
|
||||||
|
// 提交写入前统一校验多选题最大选择数,避免绕过页面限制提交超量选项。
|
||||||
|
String maxMultiLimitMessage = qsvUserAnswerRecordService.checkMaxMultiLimit(qsvAnswerParam);
|
||||||
|
if (maxMultiLimitMessage != null) {
|
||||||
|
return Result.error(maxMultiLimitMessage);
|
||||||
|
}
|
||||||
|
float totalScore = 0;
|
||||||
|
boolean isQuiz = "QUIZ".equals(activity.getCategory());
|
||||||
|
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
|
||||||
|
// 将每道题的选项、填空内容和得分写入 extJson,保持答题记录结构统一。
|
||||||
|
JSONObject entries = extJson.get(subject.getId(), JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(entries)) {
|
||||||
|
entries = new JSONObject();
|
||||||
|
}
|
||||||
|
entries.set("optionIds", subject.getUserSelectOptionIds());
|
||||||
|
entries.set("text", subject.getUserFillContent());
|
||||||
|
entries.set("optionFillContents", subject.getOptionFillContents());
|
||||||
|
if (isQuiz) {
|
||||||
|
QsvCheckAnswerResult answerResult = qsvQuizService.calcScore(subject.getId(), subject.getUserSelectOptionIds());
|
||||||
|
entries.set("score", answerResult.getScore());
|
||||||
|
entries.set("isCorrect", answerResult.isCorrect());
|
||||||
|
totalScore += answerResult.getScore();
|
||||||
|
}
|
||||||
|
extJson.set(subject.getId(), entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
answerRecord.setExtJson(extJson);
|
||||||
|
if (isQuiz) {
|
||||||
|
answerRecord.setTotalScore(totalScore);
|
||||||
|
answerRecord.setAnswerTime(qsvAnswerParam.getAnswerTime());
|
||||||
|
}
|
||||||
|
answerRecord.setAttemptDate(new Date());
|
||||||
|
answerRecord.setSubmitTime(new Date());
|
||||||
|
answerRecord.setIsFinish(true);
|
||||||
|
dao.update(answerRecord);
|
||||||
|
if (isQuiz) {
|
||||||
|
qsvUserAnswerRecordService.calcByScoreMode(qsvAnswerParam.getActivityId(), SecurityUtil.getUserId());
|
||||||
|
}
|
||||||
|
return Result.success("提交成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkActivityStatus(QsvActivity activity) {
|
||||||
|
if (activity == null) {
|
||||||
|
return Result.error("调查不存在");
|
||||||
|
}
|
||||||
|
if (Boolean.FALSE.equals(activity.getEnabled())) {
|
||||||
|
return Result.error("活动已关闭,暂不能参与");
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.getStartTime() != null && activity.getStartTime().after(now)) {
|
||||||
|
return Result.error("调查尚未开始,请在开始后再参与");
|
||||||
|
}
|
||||||
|
if (activity.getEndTime() != null && activity.getEndTime().before(now)) {
|
||||||
|
return Result.error("调查已结束");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkUserScope(QsvActivity activity) {
|
||||||
|
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
if (count != 1) {
|
||||||
|
return Result.error("您无需参加此次调查,感谢您的关注!");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NutMap buildOnlineRow(QsvActivity activity, List<QsvUserAnswerRecord> records) {
|
||||||
|
boolean isAnswered = records.stream().anyMatch((record) -> Boolean.TRUE.equals(record.getIsFinish()));
|
||||||
|
boolean hasUnfinished = records.stream().anyMatch((record) -> !Boolean.TRUE.equals(record.getIsFinish()));
|
||||||
|
boolean canRepeat = "QUIZ".equals(activity.getCategory())
|
||||||
|
&& Boolean.TRUE.equals(activity.getRepeatable())
|
||||||
|
&& ("SCHEDULED".equals(activity.getMode()) || ("REGULAR".equals(activity.getMode()) && "ALL".equals(activity.getDisplayMode())));
|
||||||
|
boolean randomDisplayMode = "QUIZ".equals(activity.getCategory())
|
||||||
|
&& "REGULAR".equals(activity.getMode())
|
||||||
|
&& "RANDOM".equals(activity.getDisplayMode());
|
||||||
|
int maxAttempts = activity.getMaxAttempts() == null || activity.getMaxAttempts() < 1 ? 1 : activity.getMaxAttempts();
|
||||||
|
int totalRandom = activity.getTotalRandom() == null || activity.getTotalRandom() < 1 ? 1 : activity.getTotalRandom();
|
||||||
|
long attemptCount = records.size();
|
||||||
|
if ("SCHEDULED".equals(activity.getMode())) {
|
||||||
|
attemptCount = records.stream()
|
||||||
|
.filter((record) -> record.getAttemptDate() != null && DateUtil.isSameDay(record.getAttemptDate(), DateUtil.date()))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
boolean notStarted = activity.getStartTime() != null && activity.getStartTime().after(now);
|
||||||
|
boolean ended = activity.getEndTime() != null && activity.getEndTime().before(now);
|
||||||
|
boolean activityAvailable = Boolean.TRUE.equals(activity.getEnabled()) && !notStarted && !ended;
|
||||||
|
boolean canAnswerByAttempt = hasUnfinished || !isAnswered || (canRepeat && attemptCount < maxAttempts) || (randomDisplayMode && attemptCount < totalRandom);
|
||||||
|
boolean canAnswer = activityAvailable && canAnswerByAttempt;
|
||||||
|
boolean isVote = "VOTE".equals(activity.getCategory());
|
||||||
|
String actionText;
|
||||||
|
String actionType;
|
||||||
|
String statusText;
|
||||||
|
String statusType;
|
||||||
|
if (!activityAvailable && !isAnswered) {
|
||||||
|
if (!Boolean.TRUE.equals(activity.getEnabled())) {
|
||||||
|
actionText = "已关闭";
|
||||||
|
statusText = "已关闭";
|
||||||
|
} else if (notStarted) {
|
||||||
|
actionText = isVote ? "未开始投票" : "未开始答题";
|
||||||
|
statusText = "未开始";
|
||||||
|
} else {
|
||||||
|
actionText = isVote ? "投票已结束" : "答题已结束";
|
||||||
|
statusText = "已结束";
|
||||||
|
}
|
||||||
|
actionType = "unavailable";
|
||||||
|
statusType = "info";
|
||||||
|
} else if (hasUnfinished && canAnswer) {
|
||||||
|
actionText = isVote ? "继续投票" : "继续答题";
|
||||||
|
actionType = "answer";
|
||||||
|
statusText = isVote ? "投票中" : "答题中";
|
||||||
|
statusType = "warning";
|
||||||
|
} else if (!isAnswered && canAnswer) {
|
||||||
|
actionText = isVote ? "投票" : "答题";
|
||||||
|
actionType = "answer";
|
||||||
|
statusText = isVote ? "未投票" : "未答题";
|
||||||
|
statusType = "warning";
|
||||||
|
} else if (canAnswer) {
|
||||||
|
actionText = isVote ? "再次投票" : "再次答题";
|
||||||
|
actionType = "answer";
|
||||||
|
statusText = isVote ? "可再次投票" : "可再次答题";
|
||||||
|
statusType = "warning";
|
||||||
|
} else {
|
||||||
|
actionText = isVote ? "查看投票" : "查看答题";
|
||||||
|
actionType = "view";
|
||||||
|
statusText = isVote ? "已投票" : "已答题";
|
||||||
|
statusType = "success";
|
||||||
|
}
|
||||||
|
Optional<QsvUserAnswerRecord> latestFinishedRecord = records.stream()
|
||||||
|
.filter((record) -> Boolean.TRUE.equals(record.getIsFinish()))
|
||||||
|
.max((first, second) -> {
|
||||||
|
long firstTime = first.getSubmitTime() == null ? 0 : first.getSubmitTime().getTime();
|
||||||
|
long secondTime = second.getSubmitTime() == null ? 0 : second.getSubmitTime().getTime();
|
||||||
|
return Long.compare(firstTime, secondTime);
|
||||||
|
});
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("id", activity.getId())
|
||||||
|
.addv("title", activity.getTitle())
|
||||||
|
.addv("category", activity.getCategory())
|
||||||
|
.addv("startTime", activity.getStartTime())
|
||||||
|
.addv("endTime", activity.getEndTime())
|
||||||
|
.addv("enabled", activity.getEnabled())
|
||||||
|
.addv("isAnswered", isAnswered)
|
||||||
|
.addv("hasUnfinished", hasUnfinished)
|
||||||
|
.addv("canAnswer", canAnswer)
|
||||||
|
.addv("notStarted", notStarted)
|
||||||
|
.addv("ended", ended)
|
||||||
|
.addv("canRepeat", canRepeat)
|
||||||
|
.addv("answeredCount", records.stream().filter((record) -> Boolean.TRUE.equals(record.getIsFinish())).count())
|
||||||
|
.addv("remainingAttempts", canRepeat ? Math.max(maxAttempts - attemptCount, 0) : (randomDisplayMode ? Math.max(totalRandom - attemptCount, 0) : 0))
|
||||||
|
.addv("actionText", actionText)
|
||||||
|
.addv("actionType", actionType)
|
||||||
|
.addv("statusText", statusText)
|
||||||
|
.addv("statusType", statusType)
|
||||||
|
.addv("totalScore", latestFinishedRecord.map(QsvUserAnswerRecord::getTotalScore).orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private QsvUserAnswerRecord findCurrentAnswerRecord(String activityId, boolean viewOnly) {
|
||||||
|
Cnd cnd = Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId());
|
||||||
|
if (viewOnly) {
|
||||||
|
cnd.and("isFinish", "=", true).desc("submitTime").desc("createdAt");
|
||||||
|
} else {
|
||||||
|
cnd.and(Cnd.exps("isFinish", "=", false).or("isFinish", "is", null)).desc("createdAt");
|
||||||
|
}
|
||||||
|
return dao.fetch(QsvUserAnswerRecord.class, cnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private QsvUserAnswerRecord prepareAnswerRecord(QsvActivity activity, QsvUserAnswerRecord answerRecord) {
|
||||||
|
if (answerRecord != null) {
|
||||||
|
return answerRecord;
|
||||||
|
}
|
||||||
|
if ("QUIZ".equals(activity.getCategory())
|
||||||
|
&& "REGULAR".equals(activity.getMode())
|
||||||
|
&& "RANDOM".equals(activity.getDisplayMode())) {
|
||||||
|
return createRandomAnswerRecord(activity);
|
||||||
|
}
|
||||||
|
List<QsvSubject> activitySubjects = queryActivitySubjectsForSync(activity);
|
||||||
|
return qsvUserAnswerRecordService.insertRecord(activity.getId(), activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private QsvUserAnswerRecord createRandomAnswerRecord(QsvActivity activity) {
|
||||||
|
Integer randomCount = activity.getRandomCount();
|
||||||
|
Integer totalRandom = activity.getTotalRandom();
|
||||||
|
if (randomCount == null || randomCount < 1) {
|
||||||
|
return qsvUserAnswerRecordService.insertRecord(activity.getId(), new ArrayList<>());
|
||||||
|
}
|
||||||
|
if (totalRandom == null || totalRandom < 1) {
|
||||||
|
totalRandom = 1;
|
||||||
|
}
|
||||||
|
List<QsvUserAnswerRecord> answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activity.getId())
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
if (answerRecords.size() >= totalRandom) {
|
||||||
|
return answerRecords.stream()
|
||||||
|
.max((first, second) -> Integer.compare(first.getRandomNumber() == null ? 0 : first.getRandomNumber(), second.getRandomNumber() == null ? 0 : second.getRandomNumber()))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
List<String> usedSubjectIds = answerRecords.stream()
|
||||||
|
.filter((record) -> record.getSubjectIds() != null)
|
||||||
|
.flatMap((record) -> record.getSubjectIds().stream())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Cnd subjectCnd = Cnd.where("activityId", "=", activity.getId());
|
||||||
|
if (!usedSubjectIds.isEmpty()) {
|
||||||
|
subjectCnd.and("id", "not in", usedSubjectIds);
|
||||||
|
}
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, subjectCnd.asc("sortNum"));
|
||||||
|
if (subjects.size() < randomCount) {
|
||||||
|
throw new RuntimeException("题目数不够,无法生成题目");
|
||||||
|
}
|
||||||
|
Collections.shuffle(subjects);
|
||||||
|
List<String> randomSubjectIds = subjects.subList(0, randomCount).stream().map(QsvSubject::getId).collect(Collectors.toList());
|
||||||
|
return qsvUserAnswerRecordService.insertRecord(activity.getId(), randomSubjectIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QsvSubject> queryActivitySubjectsForSync(QsvActivity activity) {
|
||||||
|
Cnd cnd = Cnd.where("activityId", "=", activity.getId());
|
||||||
|
if ("QUIZ".equals(activity.getCategory())
|
||||||
|
&& "SCHEDULED".equals(activity.getMode())) {
|
||||||
|
cnd.and("displayDate", "=", DateUtil.today());
|
||||||
|
}
|
||||||
|
return dao.query(QsvSubject.class, cnd.asc("sortNum"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncAnswerRecordSubjects(QsvUserAnswerRecord answerRecord, QsvActivity activity, List<QsvSubject> activitySubjects) {
|
||||||
|
if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> currentSubjectIds = activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList());
|
||||||
|
List<String> recordSubjectIds = answerRecord.getSubjectIds();
|
||||||
|
if (recordSubjectIds == null) {
|
||||||
|
recordSubjectIds = new ArrayList<>();
|
||||||
|
}
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
extJson = new JSONObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean randomDisplayMode = "QUIZ".equals(activity.getCategory())
|
||||||
|
&& "REGULAR".equals(activity.getMode())
|
||||||
|
&& "RANDOM".equals(activity.getDisplayMode());
|
||||||
|
LinkedHashSet<String> currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds);
|
||||||
|
List<String> normalizedSubjectIds = recordSubjectIds.stream()
|
||||||
|
.filter(currentSubjectIdSet::contains)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (randomDisplayMode) {
|
||||||
|
currentSubjectIds = normalizedSubjectIds;
|
||||||
|
} else {
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (!normalizedSubjectIds.contains(subjectId)) {
|
||||||
|
normalizedSubjectIds.add(subjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean changed = !recordSubjectIds.equals(normalizedSubjectIds);
|
||||||
|
boolean subjectOrderLocked = Boolean.TRUE.equals(extJson.get("_subjectOrderLocked"));
|
||||||
|
if ("QUIZ".equals(activity.getCategory())
|
||||||
|
&& Boolean.TRUE.equals(activity.getShuffleSubject())
|
||||||
|
&& !subjectOrderLocked
|
||||||
|
&& isAnswerRecordEmpty(extJson, currentSubjectIds)) {
|
||||||
|
Collections.shuffle(normalizedSubjectIds);
|
||||||
|
extJson.set("_subjectOrderLocked", true);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (ObjectUtil.isEmpty(extJson.get(subjectId, JSONObject.class))) {
|
||||||
|
extJson.set(subjectId, Dict.create()
|
||||||
|
.set("optionIds", new ArrayList<>())
|
||||||
|
.set("text", null)
|
||||||
|
.set("optionFillContents", new JSONObject()));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
answerRecord.setSubjectIds(normalizedSubjectIds);
|
||||||
|
answerRecord.setExtJson(extJson);
|
||||||
|
dao.update(answerRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAnswerRecordEmpty(JSONObject extJson, List<String> subjectIds) {
|
||||||
|
for (String subjectId : subjectIds) {
|
||||||
|
JSONObject answer = extJson.get(subjectId, JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(answer)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ObjectUtil.isNotEmpty(answer.get("optionIds"))
|
||||||
|
|| ObjectUtil.isNotEmpty(answer.get("text"))
|
||||||
|
|| ObjectUtil.isNotEmpty(answer.get("optionFillContents"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
|
||||||
|
if (subjectIds == null || subjectIds.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
|
||||||
|
Map<String, QsvSubject> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(QsvSubject::getId, Function.identity(), (oldValue, newValue) -> oldValue));
|
||||||
|
return subjectIds.stream()
|
||||||
|
.map(subjectMap::get)
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private NutMap buildVoteStats(String activityId) {
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
|
|
||||||
|
Map<String, NutMap> optionStats = new LinkedHashMap<>();
|
||||||
|
for (QsvSubject subject : subjects) {
|
||||||
|
if (subject.getOptions() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (QsvOption option : subject.getOptions()) {
|
||||||
|
optionStats.put(option.getId(), NutMap.NEW()
|
||||||
|
.addv("optionId", option.getId())
|
||||||
|
.addv("subjectId", subject.getId())
|
||||||
|
.addv("text", option.getText())
|
||||||
|
.addv("imgUrl", option.getImgUrl())
|
||||||
|
.addv("votes", 0)
|
||||||
|
.addv("percent", 0D));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<QsvUserAnswerRecord> records = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
for (QsvUserAnswerRecord record : records) {
|
||||||
|
JSONObject extJson = record.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (QsvSubject subject : subjects) {
|
||||||
|
JSONObject entry = extJson.get(subject.getId(), JSONObject.class);
|
||||||
|
if (entry == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> optionIds = entry.getBeanList("optionIds", String.class);
|
||||||
|
if (optionIds == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String optionId : optionIds) {
|
||||||
|
NutMap stat = optionStats.get(optionId);
|
||||||
|
if (stat != null) {
|
||||||
|
stat.put("votes", stat.getInt("votes", 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalParticipants = records.size();
|
||||||
|
List<NutMap> rankList = new ArrayList<>(optionStats.values());
|
||||||
|
for (NutMap stat : rankList) {
|
||||||
|
int votes = stat.getInt("votes", 0);
|
||||||
|
double percent = totalParticipants == 0 ? 0D : votes * 100D / totalParticipants;
|
||||||
|
stat.put("percent", percent);
|
||||||
|
}
|
||||||
|
rankList.sort((left, right) -> Integer.compare(right.getInt("votes", 0), left.getInt("votes", 0)));
|
||||||
|
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("totalParticipants", totalParticipants)
|
||||||
|
.addv("optionCount", optionStats.size())
|
||||||
|
.addv("optionStats", optionStats)
|
||||||
|
.addv("rankList", rankList);
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -4,11 +4,14 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
|||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.param.QsvQuizRankPageForm;
|
import com.budwk.app.zhgh.dayofficework.qsv.param.QsvQuizRankPageForm;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvQuizRankService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvQuizRankService;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
@@ -64,5 +67,17 @@ public class QsvQuizRankController {
|
|||||||
qsvQuizRankService.exportXlsx(pageForm, response);
|
qsvQuizRankService.exportXlsx(pageForm, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.quiz.rank.delete")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@ApiOperation("删除答题记录")
|
||||||
|
public Result deleteAnswerRecord(@Valid String id) {
|
||||||
|
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, id);
|
||||||
|
if (record == null) {
|
||||||
|
return Result.error("答题记录不存在");
|
||||||
|
}
|
||||||
|
dao.delete(QsvUserAnswerRecord.class, id);
|
||||||
|
return Result.success("删除成功");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+298
-5
@@ -1,18 +1,31 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.qsv.controller;
|
package com.budwk.app.zhgh.dayofficework.qsv.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.json.JSONArray;
|
||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvSurveyService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvSurveyService;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
@@ -21,6 +34,10 @@ import org.nutz.mvc.annotation.Ok;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@IocBean
|
@IocBean
|
||||||
@@ -43,12 +60,19 @@ public class QsvSurveyController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At("/reportPage")
|
||||||
|
@Ok("beetl:/platform/zhgh/dayofficework/qsv/survey/reportPage.html")
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
public void reportPage() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.survey")
|
@SaCheckPermission("qsv.survey")
|
||||||
@ApiOperation("分页查询")
|
@ApiOperation("分页查询")
|
||||||
public Result pageData(@Valid PageForm pageForm, Integer year) {
|
public Result pageData(@Valid PageForm pageForm, Integer year) {
|
||||||
Cnd cnd = Cnd.where("category", "=", "SURVEY");
|
Cnd cnd = Cnd.where("category", "in", new String[]{"SURVEY", "VOTE"});
|
||||||
cnd.andEX("YEAR(startTime)", "=", year);
|
cnd.andEX("YEAR(startTime)", "=", year);
|
||||||
cnd.desc("category");
|
cnd.desc("category");
|
||||||
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
|
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
|
||||||
@@ -63,6 +87,43 @@ public class QsvSurveyController {
|
|||||||
return Result.success(report);
|
return Result.success(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@ApiOperation("查询调查信息")
|
||||||
|
public Result activityInfo(@Valid String activityId) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
NutMap data = NutMap.NEW();
|
||||||
|
if (activity != null) {
|
||||||
|
data.setv("id", activity.getId());
|
||||||
|
data.setv("title", activity.getTitle());
|
||||||
|
}
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@ApiOperation("生成分类报告")
|
||||||
|
public Result categoryReport(@Valid String activityId, String conditions) {
|
||||||
|
List<NutMap> report = qsvSurveyService.categoryReport(activityId, conditions);
|
||||||
|
return Result.success(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@ApiOperation("生成交叉分析报告")
|
||||||
|
public Result crossReport(@Valid String activityId, @Valid String xSubjectIds, @Valid String ySubjectIds, String conditions) {
|
||||||
|
NutMap report = qsvSurveyService.crossReport(activityId, xSubjectIds, ySubjectIds, conditions);
|
||||||
|
return Result.success(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@ApiOperation("生成对比分析报告")
|
||||||
|
public Result compareReport(@Valid String activityId, @Valid String subjectId, @Valid String optionId) {
|
||||||
|
NutMap report = qsvSurveyService.compareReport(activityId, subjectId, optionId);
|
||||||
|
return Result.success(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@Ok("void")
|
@Ok("void")
|
||||||
@@ -72,13 +133,244 @@ public class QsvSurveyController {
|
|||||||
qsvSurveyService.exportReportXlsx(activityId, response);
|
qsvSurveyService.exportReportXlsx(activityId, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@ApiOperation("下载PDF报告")
|
||||||
|
public void exportReportPdf(String reportData, HttpServletResponse response) {
|
||||||
|
if (StrUtil.isBlank(reportData)) {
|
||||||
|
throw new IllegalArgumentException("报告数据不能为空");
|
||||||
|
}
|
||||||
|
JSONObject report = JSONUtil.parseObj(reportData);
|
||||||
|
String title = StrUtil.blankToDefault(report.getStr("title"), "分析报告");
|
||||||
|
String tabLabel = StrUtil.blankToDefault(report.getStr("tabLabel"), "分析报告");
|
||||||
|
try {
|
||||||
|
CommonDownloadUtil.download(safeFileName(title + "-" + tabLabel) + ".pdf", buildReportPdf(report), response);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("生成PDF报告失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildReportPdf(JSONObject report) throws IOException {
|
||||||
|
try (PDDocument document = new PDDocument(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||||
|
PdfReportWriter writer = new PdfReportWriter(document);
|
||||||
|
writer.title(StrUtil.blankToDefault(report.getStr("title"), "分析报告"));
|
||||||
|
writer.text("报告类型:" + StrUtil.blankToDefault(report.getStr("tabLabel"), "分析报告"), 10);
|
||||||
|
writer.text("生成时间:" + DateUtil.now(), 10);
|
||||||
|
writer.gap(8);
|
||||||
|
|
||||||
|
JSONArray sections = report.getJSONArray("sections");
|
||||||
|
if (sections == null || sections.isEmpty()) {
|
||||||
|
writer.text("暂无可下载内容", 12);
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < sections.size(); i++) {
|
||||||
|
JSONObject section = JSONUtil.parseObj(sections.get(i));
|
||||||
|
writer.section(StrUtil.blankToDefault(section.getStr("title"), "统计项"));
|
||||||
|
String description = section.getStr("description");
|
||||||
|
if (StrUtil.isNotBlank(description)) {
|
||||||
|
writer.text(description, 10);
|
||||||
|
}
|
||||||
|
String image = section.getStr("image");
|
||||||
|
if (StrUtil.isNotBlank(image)) {
|
||||||
|
writer.image(image);
|
||||||
|
}
|
||||||
|
writer.table(section.getJSONArray("rows"));
|
||||||
|
writer.gap(8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.close();
|
||||||
|
document.save(out);
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeFileName(String fileName) {
|
||||||
|
return StrUtil.blankToDefault(fileName, "分析报告").replaceAll("[\\\\/:*?\"<>|\\r\\n]", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
private PDType0Font loadChineseFont(PDDocument document, boolean bold) throws IOException {
|
||||||
|
String[] paths = bold
|
||||||
|
? new String[]{"/usr/share/fonts/chinese/simhei.ttf", "/usr/share/fonts/chinese/msyhbd.ttc", "/usr/share/fonts/chinese/simsun.ttc"}
|
||||||
|
: new String[]{"/usr/share/fonts/chinese/simfang.ttf", "/usr/share/fonts/chinese/simsun.ttc", "/usr/share/fonts/chinese/msyh.ttc"};
|
||||||
|
for (String path : paths) {
|
||||||
|
File file = new File(path);
|
||||||
|
if (file.exists()) {
|
||||||
|
return PDType0Font.load(document, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IOException("未找到可用的中文字体");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PdfReportWriter {
|
||||||
|
private final PDDocument document;
|
||||||
|
private final PDType0Font regularFont;
|
||||||
|
private final PDType0Font boldFont;
|
||||||
|
private final float margin = 42F;
|
||||||
|
private final float pageWidth = PDRectangle.A4.getWidth();
|
||||||
|
private final float pageHeight = PDRectangle.A4.getHeight();
|
||||||
|
private final float contentWidth = pageWidth - margin * 2;
|
||||||
|
private final float sectionTopGap = 14F;
|
||||||
|
private final float sectionMinHeight = 88F;
|
||||||
|
private final float tableBottomGap = 16F;
|
||||||
|
private PDPageContentStream content;
|
||||||
|
private float y;
|
||||||
|
|
||||||
|
PdfReportWriter(PDDocument document) throws IOException {
|
||||||
|
this.document = document;
|
||||||
|
this.regularFont = loadChineseFont(document, false);
|
||||||
|
this.boldFont = loadChineseFont(document, true);
|
||||||
|
newPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
void newPage() throws IOException {
|
||||||
|
if (content != null) {
|
||||||
|
content.close();
|
||||||
|
}
|
||||||
|
PDPage page = new PDPage(PDRectangle.A4);
|
||||||
|
document.addPage(page);
|
||||||
|
content = new PDPageContentStream(document, page);
|
||||||
|
y = pageHeight - margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
if (content != null) {
|
||||||
|
content.close();
|
||||||
|
content = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensure(float height) throws IOException {
|
||||||
|
if (y - height < margin) {
|
||||||
|
newPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void gap(float height) throws IOException {
|
||||||
|
ensure(height);
|
||||||
|
y -= height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void title(String text) throws IOException {
|
||||||
|
ensure(36);
|
||||||
|
drawText(text, margin, y, 18, boldFont);
|
||||||
|
y -= 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
void section(String text) throws IOException {
|
||||||
|
// Reserve enough space for the subject title and summary line before drawing the next question.
|
||||||
|
ensure(sectionMinHeight);
|
||||||
|
if (y < pageHeight - margin) {
|
||||||
|
y -= sectionTopGap;
|
||||||
|
ensure(sectionMinHeight - sectionTopGap);
|
||||||
|
}
|
||||||
|
drawText(text, margin, y, 13, boldFont);
|
||||||
|
y -= 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
void text(String text, float fontSize) throws IOException {
|
||||||
|
List<String> lines = wrap(StrUtil.blankToDefault(text, ""), fontSize, contentWidth, regularFont);
|
||||||
|
for (String line : lines) {
|
||||||
|
ensure(fontSize + 8);
|
||||||
|
drawText(line, margin, y, fontSize, regularFont);
|
||||||
|
y -= fontSize + 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void image(String dataUrl) throws IOException {
|
||||||
|
int commaIndex = dataUrl.indexOf(",");
|
||||||
|
if (commaIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byte[] bytes = Base64.getDecoder().decode(dataUrl.substring(commaIndex + 1));
|
||||||
|
PDImageXObject image = PDImageXObject.createFromByteArray(document, bytes, "chart");
|
||||||
|
float width = contentWidth;
|
||||||
|
float height = width * image.getHeight() / image.getWidth();
|
||||||
|
if (height > 260) {
|
||||||
|
height = 260;
|
||||||
|
width = height * image.getWidth() / image.getHeight();
|
||||||
|
}
|
||||||
|
ensure(height + 12);
|
||||||
|
content.drawImage(image, margin, y - height, width, height);
|
||||||
|
y -= height + 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
void table(JSONArray rows) throws IOException {
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
// Empty text-question summaries also need bottom spacing before the next question starts.
|
||||||
|
ensure(42);
|
||||||
|
text("暂无统计数据", 10);
|
||||||
|
gap(tableBottomGap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float[] widths = new float[]{contentWidth * 0.46F, contentWidth * 0.15F, contentWidth * 0.17F, contentWidth * 0.22F};
|
||||||
|
// Keep the table header with at least the first data row when a table starts near the page bottom.
|
||||||
|
ensure(48);
|
||||||
|
drawTableRow(new String[]{"选项/内容", "数量", "占比", "备注"}, widths, true);
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
JSONObject row = JSONUtil.parseObj(rows.get(i));
|
||||||
|
drawTableRow(new String[]{
|
||||||
|
StrUtil.blankToDefault(row.getStr("name"), ""),
|
||||||
|
StrUtil.blankToDefault(row.getStr("count"), ""),
|
||||||
|
StrUtil.blankToDefault(row.getStr("percent"), ""),
|
||||||
|
StrUtil.blankToDefault(row.getStr("remark"), "")
|
||||||
|
}, widths, false);
|
||||||
|
}
|
||||||
|
gap(tableBottomGap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawTableRow(String[] values, float[] widths, boolean header) throws IOException {
|
||||||
|
float rowHeight = 24F;
|
||||||
|
ensure(rowHeight);
|
||||||
|
float x = margin;
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
content.addRect(x, y - rowHeight, widths[i], rowHeight);
|
||||||
|
content.stroke();
|
||||||
|
drawText(clip(values[i], header ? 16 : 28), x + 5, y - 16, 9, header ? boldFont : regularFont);
|
||||||
|
x += widths[i];
|
||||||
|
}
|
||||||
|
y -= rowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawText(String text, float x, float y, float fontSize, PDType0Font font) throws IOException {
|
||||||
|
content.beginText();
|
||||||
|
content.setFont(font, fontSize);
|
||||||
|
content.newLineAtOffset(x, y);
|
||||||
|
content.showText(StrUtil.blankToDefault(text, "").replaceAll("[\\r\\n\\t]", " "));
|
||||||
|
content.endText();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> wrap(String text, float fontSize, float maxWidth, PDType0Font font) throws IOException {
|
||||||
|
List<String> lines = new java.util.ArrayList<>();
|
||||||
|
StringBuilder line = new StringBuilder();
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
char ch = text.charAt(i);
|
||||||
|
String next = line.toString() + ch;
|
||||||
|
if (font.getStringWidth(next) / 1000 * fontSize > maxWidth && line.length() > 0) {
|
||||||
|
lines.add(line.toString());
|
||||||
|
line.setLength(0);
|
||||||
|
}
|
||||||
|
line.append(ch);
|
||||||
|
}
|
||||||
|
if (line.length() > 0 || lines.isEmpty()) {
|
||||||
|
lines.add(line.toString());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
String clip(String text, int maxLength) {
|
||||||
|
if (text == null || text.length() <= maxLength) {
|
||||||
|
return StrUtil.blankToDefault(text, "");
|
||||||
|
}
|
||||||
|
return text.substring(0, maxLength) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.survey")
|
@SaCheckPermission("qsv.survey")
|
||||||
@ApiOperation("选项选择详情")
|
@ApiOperation("选项选择详情")
|
||||||
public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId) {
|
public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId, String conditions) {
|
||||||
List<QsvUserAnswerRecord> answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId));
|
List<NutMap> selectOptionUsers = qsvSurveyService.selectOptionUsers(activityId, subjectId, optionId, conditions);
|
||||||
List<QsvUserAnswerRecord> selectOptionUsers = answerRecords.stream().filter(ext -> ObjectUtil.isNotNull(ext.getExtJson().get(subjectId, JSONObject.class)) && ext.getExtJson().get(subjectId, JSONObject.class).getJSONArray("optionIds").contains(optionId)).toList();
|
|
||||||
return Result.success(selectOptionUsers);
|
return Result.success(selectOptionUsers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +387,7 @@ public class QsvSurveyController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.survey")
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@ApiOperation("删除用户答题记录")
|
@ApiOperation("删除用户答题记录")
|
||||||
@SLog(type = "qsv.survey", tag = "删除用户答题记录", msg = "删除用户答题记录")
|
@SLog(type = "qsv.survey", tag = "删除用户答题记录", msg = "删除用户答题记录")
|
||||||
public Result deleteUserAnswer(@Valid String id) {
|
public Result deleteUserAnswer(@Valid String id) {
|
||||||
|
|||||||
+70
-2
@@ -1,19 +1,29 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
|
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@IocBean
|
@IocBean
|
||||||
@At("/platform/h5/qsv")
|
@At("/platform/h5/qsv")
|
||||||
@@ -23,6 +33,8 @@ public class H5QsvController {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private QsvActivityService qsvActivityService;
|
private QsvActivityService qsvActivityService;
|
||||||
|
@Inject
|
||||||
|
private Dao dao;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("beetl:/platform/zhghh5/dayofficework/qsv/index.html")
|
@Ok("beetl:/platform/zhghh5/dayofficework/qsv/index.html")
|
||||||
@@ -33,13 +45,69 @@ public class H5QsvController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("h5.qsv")
|
@SaCheckPermission("h5.qsv")
|
||||||
public Result pageData(@Valid PageForm pageForm, @Valid String category) {
|
public Result pageData(@Valid PageForm pageForm, @Valid String category, @Param("title") String title) {
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
cnd.andEX("category", "=", category);
|
cnd.andEX("category", "=", category);
|
||||||
|
cnd.and(Cnd.likeEX("title", title));
|
||||||
|
cnd.and(Cnd.exps("enabled", "=", true).or("enabled", "is", null));
|
||||||
|
LocalDate currentYearFirstDay = LocalDate.now().withDayOfYear(1);
|
||||||
|
Date currentYearStart = Date.from(currentYearFirstDay.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||||
|
Date nextYearStart = Date.from(currentYearFirstDay.plusYears(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||||
|
cnd.and("startTime", ">=", currentYearStart);
|
||||||
|
cnd.and("startTime", "<", nextYearStart);
|
||||||
cnd.desc("startTime");
|
cnd.desc("startTime");
|
||||||
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
|
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
|
||||||
|
fillAnsweredStatus(pagination);
|
||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fillAnsweredStatus(Pagination pagination) {
|
||||||
|
List<NutMap> rows = pagination.getList();
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> activityIds = rows.stream()
|
||||||
|
.map(this::getRowActivityId)
|
||||||
|
.filter(id -> id != null && !id.isBlank())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (activityIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> answeredActivityIds = dao.query(QsvUserAnswerRecord.class, Cnd.where("userId", "=", SecurityUtil.getUserId())
|
||||||
|
.and("activityId", "in", activityIds)
|
||||||
|
.and("isFinish", "=", true))
|
||||||
|
.stream()
|
||||||
|
.map(QsvUserAnswerRecord::getActivityId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
rows.forEach(row -> {
|
||||||
|
boolean isAnswered = answeredActivityIds.contains(getRowActivityId(row));
|
||||||
|
row.put("isAnswered", isAnswered);
|
||||||
|
row.put("answeredText", isAnswered ? getAnsweredText(row.getString("category")) : "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getRowActivityId(NutMap row) {
|
||||||
|
String id = row.getString("id");
|
||||||
|
if (id == null || id.isBlank()) {
|
||||||
|
id = row.getString("ID");
|
||||||
|
}
|
||||||
|
if (id == null || id.isBlank()) {
|
||||||
|
id = row.getString("activityId");
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getAnsweredText(String category) {
|
||||||
|
if ("QUIZ".equals(category)) {
|
||||||
|
return "已答题";
|
||||||
|
}
|
||||||
|
if ("SURVEY".equals(category)) {
|
||||||
|
return "已填写";
|
||||||
|
}
|
||||||
|
if ("VOTE".equals(category)) {
|
||||||
|
return "已投票";
|
||||||
|
}
|
||||||
|
return "已完成";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-1
@@ -124,7 +124,9 @@ public class H5QsvQuizController {
|
|||||||
//首次进来生成答题记录
|
//首次进来生成答题记录
|
||||||
if ("ALL".equals(displayMode)) {
|
if ("ALL".equals(displayMode)) {
|
||||||
resultSubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
resultSubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
answerRecordId = qsvUserAnswerRecordService.insertRecord(activityId, resultSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList())).getId();
|
QsvUserAnswerRecord answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, resultSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList()));
|
||||||
|
answerRecordId = answerRecord.getId();
|
||||||
|
resultSubjects = querySubjectsByRecordOrder(answerRecord.getSubjectIds());
|
||||||
} else if ("RANDOM".equals(displayMode)) {
|
} else if ("RANDOM".equals(displayMode)) {
|
||||||
//单次随机抽取题目数量
|
//单次随机抽取题目数量
|
||||||
Integer randomCount = activity.getRandomCount();
|
Integer randomCount = activity.getRandomCount();
|
||||||
@@ -333,8 +335,14 @@ public class H5QsvQuizController {
|
|||||||
@At
|
@At
|
||||||
@ApiOperation("提交答题")
|
@ApiOperation("提交答题")
|
||||||
@SLog(type = "qsv.quiz", tag = "提交答题", msg = "问卷调查提交答题")
|
@SLog(type = "qsv.quiz", tag = "提交答题", msg = "问卷调查提交答题")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
|
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
|
||||||
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
|
||||||
|
// 提交写入前统一校验多选题最大选择数,避免移动端绕过页面限制提交超量选项。
|
||||||
|
String maxMultiLimitMessage = qsvUserAnswerRecordService.checkMaxMultiLimit(qsvAnswerParam);
|
||||||
|
if (maxMultiLimitMessage != null) {
|
||||||
|
return Result.error(maxMultiLimitMessage);
|
||||||
|
}
|
||||||
JSONObject extJson = answerRecord.getExtJson();
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
|
||||||
float totalScore = 0;
|
float totalScore = 0;
|
||||||
@@ -396,4 +404,17 @@ public class H5QsvQuizController {
|
|||||||
return Result.success(result);
|
return Result.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
|
||||||
|
if (subjectIds == null || subjectIds.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
|
||||||
|
Map<String, QsvSubject> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(QsvSubject::getId, java.util.function.Function.identity(), (oldValue, newValue) -> oldValue));
|
||||||
|
return subjectIds.stream()
|
||||||
|
.map(subjectMap::get)
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-3
@@ -12,6 +12,8 @@ import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam;
|
|||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
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.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
@@ -22,7 +24,11 @@ import org.nutz.mvc.annotation.Param;
|
|||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@IocBean
|
@IocBean
|
||||||
@At("/platform/h5/qsv/survey")
|
@At("/platform/h5/qsv/survey")
|
||||||
@@ -40,8 +46,13 @@ public class H5QsvSurveyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result subjects(String activityId) {
|
public Result subjects(String activityId) {
|
||||||
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
Result activityStatusResult = checkActivityStatus(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
|
||||||
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
|
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
|
||||||
.and("userId", "=", SecurityUtil.getUserId()));
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
@@ -51,20 +62,21 @@ public class H5QsvSurveyController {
|
|||||||
|
|
||||||
String answerRecordId = null;
|
String answerRecordId = null;
|
||||||
|
|
||||||
|
List<QsvSubject> activitySubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
.and("userId", "=", SecurityUtil.getUserId()));
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
if (answerRecord == null) {
|
if (answerRecord == null) {
|
||||||
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
qsvUserAnswerRecordService.insertRecord(activityId, activitySubjects.stream().map(QsvSubject::getId).toList());
|
||||||
qsvUserAnswerRecordService.insertRecord(activityId, subjects.stream().map(QsvSubject::getId).toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QsvUserAnswerRecord answerRecord2 = dao.fetch(QsvUserAnswerRecord.class,
|
QsvUserAnswerRecord answerRecord2 = dao.fetch(QsvUserAnswerRecord.class,
|
||||||
Cnd.where("activityId", "=", activityId)
|
Cnd.where("activityId", "=", activityId)
|
||||||
.and("userId", "=", SecurityUtil.getUserId()));
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
syncAnswerRecordSubjects(answerRecord2, activitySubjects);
|
||||||
|
|
||||||
answerRecordId = answerRecord2.getId();
|
answerRecordId = answerRecord2.getId();
|
||||||
|
|
||||||
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", answerRecord2.getSubjectIds()).asc("sortNum"));
|
List<QsvSubject> subjects = querySubjectsByRecordOrder(answerRecord2.getSubjectIds());
|
||||||
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
|
|
||||||
for (QsvSubject subject : subjects) {
|
for (QsvSubject subject : subjects) {
|
||||||
@@ -77,6 +89,95 @@ public class H5QsvSurveyController {
|
|||||||
return Result.success(result);
|
return Result.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验调查活动是否处于可答题状态。activity 为空表示活动不存在;
|
||||||
|
* enabled 为 false 表示活动已关闭,当前时间早于 startTime 表示未开始,当前时间晚于 endTime 表示已结束。
|
||||||
|
*
|
||||||
|
* @param activity 当前调查活动,包含开启状态、开始时间和结束时间
|
||||||
|
* @return 状态或时间不允许答题时返回错误结果,允许答题时返回 null
|
||||||
|
*/
|
||||||
|
private Result checkActivityStatus(QsvActivity activity) {
|
||||||
|
if (activity == null) {
|
||||||
|
return Result.error("调查不存在");
|
||||||
|
}
|
||||||
|
if (Boolean.FALSE.equals(activity.getEnabled())) {
|
||||||
|
return Result.error("活动已关闭,暂不能参与");
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.getStartTime() != null && activity.getStartTime().after(now)) {
|
||||||
|
return Result.error("调查尚未开始,请在开始后再参与");
|
||||||
|
}
|
||||||
|
if (activity.getEndTime() != null && activity.getEndTime().before(now)) {
|
||||||
|
return Result.error("调查已结束");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步问卷当前题目到未完成答题记录。后台新增题目后,已进入过问卷的用户记录里没有新题ID,
|
||||||
|
* 这里补齐 subjectIds 和 extJson,保证手机端能拿到最新题目并正常提交答案。
|
||||||
|
*
|
||||||
|
* @param answerRecord 用户当前问卷答题记录
|
||||||
|
* @param activitySubjects 当前活动下按排序查询出的最新题目列表
|
||||||
|
*/
|
||||||
|
private void syncAnswerRecordSubjects(QsvUserAnswerRecord answerRecord, List<QsvSubject> activitySubjects) {
|
||||||
|
if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> currentSubjectIds = activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList());
|
||||||
|
List<String> recordSubjectIds = answerRecord.getSubjectIds();
|
||||||
|
if (recordSubjectIds == null) {
|
||||||
|
recordSubjectIds = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
extJson = new JSONObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedHashSet<String> currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds);
|
||||||
|
List<String> normalizedSubjectIds = recordSubjectIds.stream()
|
||||||
|
.filter(currentSubjectIdSet::contains)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (!normalizedSubjectIds.contains(subjectId)) {
|
||||||
|
normalizedSubjectIds.add(subjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean changed = !recordSubjectIds.equals(normalizedSubjectIds);
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (ObjectUtil.isEmpty(extJson.get(subjectId, JSONObject.class))) {
|
||||||
|
JSONObject entry = new JSONObject();
|
||||||
|
entry.set("optionIds", new ArrayList<>());
|
||||||
|
entry.set("text", null);
|
||||||
|
entry.set("optionFillContents", new JSONObject());
|
||||||
|
extJson.set(subjectId, entry);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
answerRecord.setSubjectIds(normalizedSubjectIds);
|
||||||
|
answerRecord.setExtJson(extJson);
|
||||||
|
dao.update(answerRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
|
||||||
|
if (subjectIds == null || subjectIds.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
|
||||||
|
Map<String, QsvSubject> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(QsvSubject::getId, Function.identity(), (oldValue, newValue) -> oldValue));
|
||||||
|
return subjectIds.stream()
|
||||||
|
.map(subjectMap::get)
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
public Result answerRecord(@Valid String answerRecordId) {
|
public Result answerRecord(@Valid String answerRecordId) {
|
||||||
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
|
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
|
||||||
@@ -84,8 +185,20 @@ public class H5QsvSurveyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
|
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId());
|
||||||
|
Result activityStatusResult = checkActivityStatus(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
|
||||||
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
|
||||||
|
// 提交写入前统一校验多选题最大选择数,避免移动端绕过页面限制提交超量选项。
|
||||||
|
String maxMultiLimitMessage = qsvUserAnswerRecordService.checkMaxMultiLimit(qsvAnswerParam);
|
||||||
|
if (maxMultiLimitMessage != null) {
|
||||||
|
return Result.error(maxMultiLimitMessage);
|
||||||
|
}
|
||||||
JSONObject extJson = answerRecord.getExtJson();
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
|
||||||
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
|
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
|
||||||
@@ -93,6 +206,7 @@ public class H5QsvSurveyController {
|
|||||||
if (ObjectUtil.isNotEmpty(entries)) {
|
if (ObjectUtil.isNotEmpty(entries)) {
|
||||||
entries.set("optionIds", subject.getUserSelectOptionIds());
|
entries.set("optionIds", subject.getUserSelectOptionIds());
|
||||||
entries.set("text", subject.getUserFillContent());
|
entries.set("text", subject.getUserFillContent());
|
||||||
|
entries.set("optionFillContents", subject.getOptionFillContents());
|
||||||
}
|
}
|
||||||
extJson.set(subject.getId(), entries);
|
extJson.set(subject.getId(), entries);
|
||||||
}
|
}
|
||||||
|
|||||||
+339
@@ -0,0 +1,339 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
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;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/h5/qsv/vote")
|
||||||
|
@Ok("json:full")
|
||||||
|
/**
|
||||||
|
* 移动端投票控制器,负责投票页面、题目读取、投票提交和排行榜统计。
|
||||||
|
*/
|
||||||
|
public class H5QsvVoteController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Dao dao;
|
||||||
|
@Inject
|
||||||
|
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
|
||||||
|
|
||||||
|
@At("")
|
||||||
|
@Ok("beetl:/platform/zhghh5/dayofficework/qsv/vote/index.html")
|
||||||
|
/**
|
||||||
|
* 打开移动端投票页面。
|
||||||
|
*/
|
||||||
|
public void index() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询投票题目,并为首次进入的用户创建投票记录。
|
||||||
|
*
|
||||||
|
* @param activityId 投票活动 ID
|
||||||
|
* @return 投票活动、题目、答题记录和统计数据
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result subjects(String activityId) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
Result activityStatusResult = checkActivityReadable(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result scopeResult = checkScope(activity);
|
||||||
|
if (scopeResult != null) {
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<QsvSubject> activitySubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
if (answerRecord == null) {
|
||||||
|
// 首次进入投票时初始化记录,后续提交时复用同一条记录。
|
||||||
|
List<String> subjectIds = activitySubjects.stream()
|
||||||
|
.map(QsvSubject::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, subjectIds);
|
||||||
|
}
|
||||||
|
syncAnswerRecordSubjects(answerRecord, activitySubjects);
|
||||||
|
|
||||||
|
List<QsvSubject> subjects = querySubjectsByRecordOrder(answerRecord.getSubjectIds());
|
||||||
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
|
for (QsvSubject subject : subjects) {
|
||||||
|
if (subject.getUserSelectOptionIds() == null) {
|
||||||
|
subject.setUserSelectOptionIds(new ArrayList<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NutMap result = NutMap.NEW()
|
||||||
|
.addv("subjects", subjects)
|
||||||
|
.addv("answerRecordId", answerRecord.getId())
|
||||||
|
.addv("answerRecord", answerRecord)
|
||||||
|
.addv("activity", activity)
|
||||||
|
.addv("voteStats", buildVoteStats(activityId));
|
||||||
|
return Result.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
/**
|
||||||
|
* 查询投票记录,用于恢复用户已选择的投票项。
|
||||||
|
*
|
||||||
|
* @param answerRecordId 投票记录 ID
|
||||||
|
* @return 投票记录
|
||||||
|
*/
|
||||||
|
public Result answerRecord(@Valid String answerRecordId) {
|
||||||
|
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
|
||||||
|
return Result.success(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
/**
|
||||||
|
* 查询投票排行榜和统计数据。
|
||||||
|
*
|
||||||
|
* @param activityId 投票活动 ID
|
||||||
|
* @return 投票统计数据
|
||||||
|
*/
|
||||||
|
public Result rank(@Valid String activityId) {
|
||||||
|
return Result.success(buildVoteStats(activityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交移动端投票结果,投票成功后标记记录为完成。
|
||||||
|
*
|
||||||
|
* @param qsvAnswerParam 用户提交的投票答案
|
||||||
|
* @return 提交结果
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId());
|
||||||
|
Result activityStatusResult = checkActivityWritable(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
|
||||||
|
if (answerRecord == null || !SecurityUtil.getUserId().equals(answerRecord.getUserId())) {
|
||||||
|
return Result.error("\u6295\u7968\u8bb0\u5f55\u4e0d\u5b58\u5728");
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(answerRecord.getIsFinish())) {
|
||||||
|
return Result.error("\u60a8\u5df2\u5b8c\u6210\u6295\u7968");
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
extJson = new JSONObject();
|
||||||
|
}
|
||||||
|
// 提交写入前统一校验多选题最大选择数,避免移动端投票绕过页面限制提交超量选项。
|
||||||
|
String maxMultiLimitMessage = qsvUserAnswerRecordService.checkMaxMultiLimit(qsvAnswerParam);
|
||||||
|
if (maxMultiLimitMessage != null) {
|
||||||
|
return Result.error(maxMultiLimitMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
|
||||||
|
// 逐题保存用户选择和补充内容,便于统计票数与查看详情。
|
||||||
|
JSONObject entries = extJson.get(subject.getId(), JSONObject.class);
|
||||||
|
if (entries == null) {
|
||||||
|
entries = new JSONObject();
|
||||||
|
}
|
||||||
|
entries.set("optionIds", subject.getUserSelectOptionIds());
|
||||||
|
entries.set("text", subject.getUserFillContent());
|
||||||
|
entries.set("optionFillContents", subject.getOptionFillContents());
|
||||||
|
extJson.set(subject.getId(), entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
answerRecord.setExtJson(extJson);
|
||||||
|
answerRecord.setAttemptDate(new Date());
|
||||||
|
answerRecord.setSubmitTime(new Date());
|
||||||
|
answerRecord.setIsFinish(true);
|
||||||
|
dao.update(answerRecord);
|
||||||
|
|
||||||
|
return Result.success("\u6295\u7968\u6210\u529f");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkActivityReadable(QsvActivity activity) {
|
||||||
|
if (activity == null) {
|
||||||
|
return Result.error("\u6295\u7968\u4e0d\u5b58\u5728");
|
||||||
|
}
|
||||||
|
if (Boolean.FALSE.equals(activity.getEnabled())) {
|
||||||
|
return Result.error("\u6d3b\u52a8\u5df2\u5173\u95ed\uff0c\u6682\u4e0d\u80fd\u53c2\u4e0e");
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.getStartTime() != null && activity.getStartTime().after(now)) {
|
||||||
|
return Result.error("\u6295\u7968\u5c1a\u672a\u5f00\u59cb\uff0c\u8bf7\u5728\u5f00\u59cb\u540e\u518d\u53c2\u4e0e");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkActivityWritable(QsvActivity activity) {
|
||||||
|
Result readableResult = checkActivityReadable(activity);
|
||||||
|
if (readableResult != null) {
|
||||||
|
return readableResult;
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.getEndTime() != null && activity.getEndTime().before(now)) {
|
||||||
|
return Result.error("\u6295\u7968\u5df2\u7ed3\u675f");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkScope(QsvActivity activity) {
|
||||||
|
if (activity.getGroupId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
if (count != 1) {
|
||||||
|
return Result.error("\u60a8\u65e0\u9700\u53c2\u52a0\u6b64\u6b21\u6295\u7968\uff0c\u611f\u8c22\u60a8\u7684\u5173\u6ce8\uff01");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncAnswerRecordSubjects(QsvUserAnswerRecord answerRecord, List<QsvSubject> activitySubjects) {
|
||||||
|
if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 后台调整投票题目后,未完成记录需要同步当前有效题目,避免提交旧题目。
|
||||||
|
List<String> currentSubjectIds = activitySubjects.stream()
|
||||||
|
.map(QsvSubject::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<String> recordSubjectIds = answerRecord.getSubjectIds();
|
||||||
|
if (recordSubjectIds == null) {
|
||||||
|
recordSubjectIds = new ArrayList<>();
|
||||||
|
}
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
extJson = new JSONObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedHashSet<String> currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds);
|
||||||
|
List<String> normalizedSubjectIds = recordSubjectIds.stream()
|
||||||
|
.filter(currentSubjectIdSet::contains)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (!normalizedSubjectIds.contains(subjectId)) {
|
||||||
|
normalizedSubjectIds.add(subjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean changed = !recordSubjectIds.equals(normalizedSubjectIds);
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (ObjectUtil.isEmpty(extJson.get(subjectId, JSONObject.class))) {
|
||||||
|
JSONObject entry = new JSONObject();
|
||||||
|
entry.set("optionIds", new ArrayList<>());
|
||||||
|
entry.set("text", null);
|
||||||
|
entry.set("optionFillContents", new JSONObject());
|
||||||
|
extJson.set(subjectId, entry);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
answerRecord.setSubjectIds(normalizedSubjectIds);
|
||||||
|
answerRecord.setExtJson(extJson);
|
||||||
|
dao.update(answerRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
|
||||||
|
if (subjectIds == null || subjectIds.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
|
||||||
|
Map<String, QsvSubject> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(QsvSubject::getId, Function.identity(), (oldValue, newValue) -> oldValue));
|
||||||
|
return subjectIds.stream()
|
||||||
|
.map(subjectMap::get)
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private NutMap buildVoteStats(String activityId) {
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
|
|
||||||
|
Map<String, NutMap> optionStats = new LinkedHashMap<>();
|
||||||
|
for (QsvSubject subject : subjects) {
|
||||||
|
if (subject.getOptions() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (QsvOption option : subject.getOptions()) {
|
||||||
|
optionStats.put(option.getId(), NutMap.NEW()
|
||||||
|
.addv("optionId", option.getId())
|
||||||
|
.addv("subjectId", subject.getId())
|
||||||
|
.addv("text", option.getText())
|
||||||
|
.addv("imgUrl", option.getImgUrl())
|
||||||
|
.addv("votes", 0)
|
||||||
|
.addv("percent", 0D));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<QsvUserAnswerRecord> records = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
for (QsvUserAnswerRecord record : records) {
|
||||||
|
JSONObject extJson = record.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (QsvSubject subject : subjects) {
|
||||||
|
JSONObject entry = extJson.get(subject.getId(), JSONObject.class);
|
||||||
|
if (entry == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> optionIds = entry.getBeanList("optionIds", String.class);
|
||||||
|
if (optionIds == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String optionId : optionIds) {
|
||||||
|
NutMap stat = optionStats.get(optionId);
|
||||||
|
if (stat != null) {
|
||||||
|
stat.put("votes", stat.getInt("votes", 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalParticipants = records.size();
|
||||||
|
List<NutMap> rankList = new ArrayList<>(optionStats.values());
|
||||||
|
for (NutMap stat : rankList) {
|
||||||
|
int votes = stat.getInt("votes", 0);
|
||||||
|
double percent = totalParticipants == 0 ? 0D : votes * 100D / totalParticipants;
|
||||||
|
stat.put("percent", percent);
|
||||||
|
}
|
||||||
|
rankList.sort((left, right) -> Integer.compare(right.getInt("votes", 0), left.getInt("votes", 0)));
|
||||||
|
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("totalParticipants", totalParticipants)
|
||||||
|
.addv("optionCount", optionStats.size())
|
||||||
|
.addv("optionStats", optionStats)
|
||||||
|
.addv("rankList", rankList);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.qsv.models;
|
package com.budwk.app.zhgh.dayofficework.qsv.models;
|
||||||
|
|
||||||
import com.budwk.app.base.model.BaseModel;
|
import com.budwk.app.base.model.BaseModel;
|
||||||
|
import com.budwk.app.sys.models.Sys_home_activity;
|
||||||
|
import com.budwk.app.sys.services.SysHomeConvert;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import org.nutz.dao.entity.annotation.*;
|
import org.nutz.dao.entity.annotation.*;
|
||||||
@@ -14,7 +16,7 @@ import java.util.Date;
|
|||||||
@Table("qsv_activity")
|
@Table("qsv_activity")
|
||||||
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
||||||
@Comment("问卷调查投票活动表")
|
@Comment("问卷调查投票活动表")
|
||||||
public class QsvActivity extends BaseModel implements Serializable {
|
public class QsvActivity extends BaseModel implements Serializable, SysHomeConvert {
|
||||||
@Name
|
@Name
|
||||||
@Comment("ID")
|
@Comment("ID")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
@@ -46,6 +48,12 @@ public class QsvActivity extends BaseModel implements Serializable {
|
|||||||
@ColDefine(type = ColType.DATETIME)
|
@ColDefine(type = ColType.DATETIME)
|
||||||
private Date endTime;
|
private Date endTime;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("是否开启")
|
||||||
|
@ColDefine(type = ColType.BOOLEAN)
|
||||||
|
@Default("1")
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("活动分组ID")
|
@Comment("活动分组ID")
|
||||||
@ColDefine(type = ColType.INT)
|
@ColDefine(type = ColType.INT)
|
||||||
@@ -107,4 +115,35 @@ public class QsvActivity extends BaseModel implements Serializable {
|
|||||||
@Comment("封面图片")
|
@Comment("封面图片")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 255)
|
@ColDefine(type = ColType.VARCHAR, width = 255)
|
||||||
private String cover;
|
private String cover;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Sys_home_activity covertToSysHomeActivity() {
|
||||||
|
Sys_home_activity sysHomeActivity = new Sys_home_activity();
|
||||||
|
sysHomeActivity.setId(this.getId());
|
||||||
|
sysHomeActivity.setName(this.getTitle());
|
||||||
|
sysHomeActivity.setCover(this.getCover());
|
||||||
|
sysHomeActivity.setContent(this.getDescription());
|
||||||
|
sysHomeActivity.setUrl("/platform/qsv/online");
|
||||||
|
sysHomeActivity.setH5Url(getH5Url());
|
||||||
|
sysHomeActivity.setStartDate(this.getStartTime());
|
||||||
|
sysHomeActivity.setEndDate(this.getEndTime());
|
||||||
|
sysHomeActivity.setAllowUserGroupId(this.getGroupId());
|
||||||
|
sysHomeActivity.setEnable(!Boolean.FALSE.equals(this.getEnabled()));
|
||||||
|
sysHomeActivity.setClassPath(this.getClass().getPackageName() + this.getClass().getName());
|
||||||
|
return sysHomeActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据活动类型生成移动端首页跳转地址。
|
||||||
|
* QUIZ 跳转答题页,SURVEY 跳转调查页,VOTE 预留投票页地址,返回值为移动端路由字符串。
|
||||||
|
*/
|
||||||
|
private String getH5Url() {
|
||||||
|
if ("SURVEY".equals(this.getCategory())) {
|
||||||
|
return "/platform/h5/qsv/survey?id=" + this.getId();
|
||||||
|
}
|
||||||
|
if ("VOTE".equals(this.getCategory())) {
|
||||||
|
return "/platform/h5/qsv/vote?id=" + this.getId();
|
||||||
|
}
|
||||||
|
return "/platform/h5/qsv/quiz?id=" + this.getId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ public class QsvOption extends BaseModel implements Serializable {
|
|||||||
@ColDefine(customType = "longtext")
|
@ColDefine(customType = "longtext")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("选中后是否需要填写补充内容")
|
||||||
|
@ColDefine(type = ColType.BOOLEAN)
|
||||||
|
private Boolean fillRequired;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("补充内容输入提示")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 255)
|
||||||
|
private String fillPlaceholder;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("排序")
|
@Comment("排序")
|
||||||
@ColDefine(type = ColType.INT)
|
@ColDefine(type = ColType.INT)
|
||||||
|
|||||||
@@ -34,10 +34,15 @@ public class QsvSubject extends BaseModel implements Serializable {
|
|||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("题目类型(single, multi, judge, fill)")
|
@Comment("题目类型(radio单选,checkbox多选,text填空)")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("题目组件展示类型(RADIO普通单选,DROPDOWN下拉选择)")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 30)
|
||||||
|
private String componentType;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("正确答案")
|
@Comment("正确答案")
|
||||||
@ColDefine(type = ColType.MYSQL_JSON)
|
@ColDefine(type = ColType.MYSQL_JSON)
|
||||||
@@ -58,11 +63,41 @@ public class QsvSubject extends BaseModel implements Serializable {
|
|||||||
@ColDefine(type = ColType.INT, width = 1)
|
@ColDefine(type = ColType.INT, width = 1)
|
||||||
private Integer maxMulti;
|
private Integer maxMulti;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("投票选项排列方式")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
|
private String optionLayout;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("投票选项横向列数")
|
||||||
|
@ColDefine(type = ColType.INT, width = 1)
|
||||||
|
private Integer optionColumns;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("排序")
|
@Comment("排序")
|
||||||
@ColDefine(type = ColType.INT, width = 1)
|
@ColDefine(type = ColType.INT, width = 1)
|
||||||
private Integer sortNum;
|
private Integer sortNum;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("是否启用题目隐显逻辑")
|
||||||
|
@ColDefine(type = ColType.BOOLEAN)
|
||||||
|
private Boolean visibleRuleEnabled;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("隐显逻辑处理方式(show显示,hide隐藏)")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
|
private String visibleRuleAction;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("隐显逻辑条件关系(AND并且,OR或者)")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
|
private String visibleRuleLogic;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("隐显逻辑条件列表")
|
||||||
|
@ColDefine(type = ColType.MYSQL_JSON)
|
||||||
|
private List<VisibleRuleCondition> visibleRuleConditions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选项
|
* 选项
|
||||||
*/
|
*/
|
||||||
@@ -79,4 +114,52 @@ public class QsvSubject extends BaseModel implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String userFillContent;
|
private String userFillContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目隐显逻辑条件,一条条件表示选择指定题目的指定选项后参与显示或隐藏判断。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class VisibleRuleCondition implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作为触发条件的题目ID。
|
||||||
|
*/
|
||||||
|
private String subjectId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作为触发条件的选项ID。
|
||||||
|
*/
|
||||||
|
private String optionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件节点类型:group 分组,item 条件项。
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分组内条件关系:and 并且,or 或者。
|
||||||
|
*/
|
||||||
|
private String logic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件项引用的题目ID。
|
||||||
|
*/
|
||||||
|
private String field;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较符:==、!=、contains、>、<、>=、<=。
|
||||||
|
*/
|
||||||
|
private String operator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较值,选择题为选项ID,填空题可为文本或数字。
|
||||||
|
*/
|
||||||
|
private Object value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子条件节点,支持条件分组嵌套。
|
||||||
|
*/
|
||||||
|
private List<VisibleRuleCondition> children;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.dayofficework.qsv.param;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
//答题参数
|
//答题参数
|
||||||
@@ -21,6 +22,7 @@ public class QsvAnswerParam {
|
|||||||
private String id;
|
private String id;
|
||||||
private List<String> userSelectOptionIds;
|
private List<String> userSelectOptionIds;
|
||||||
private String userFillContent;
|
private String userFillContent;
|
||||||
|
private Map<String, String> optionFillContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,26 @@ package com.budwk.app.zhgh.dayofficework.qsv.service;
|
|||||||
|
|
||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
public interface QsvActivityService extends BaseService<QsvActivity> {
|
public interface QsvActivityService extends BaseService<QsvActivity> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save activity with normal mode-change protection.
|
||||||
|
*/
|
||||||
|
QsvActivity saveActivity(QsvActivity qsvActivity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save activity and optionally bypass the answer-mode change confirmation after the user confirms.
|
||||||
|
*/
|
||||||
|
QsvActivity saveActivity(QsvActivity qsvActivity, Boolean forceSave);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether changing answer mode needs user confirmation because answer records already exist.
|
||||||
|
*/
|
||||||
|
NutMap checkAnswerModeChangeConfirm(QsvActivity qsvActivity);
|
||||||
|
|
||||||
|
void deleteActivity(String id);
|
||||||
|
|
||||||
|
void updateEnabled(String id, Boolean enabled);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ public interface QsvSurveyService extends BaseService<QsvUserAnswerRecord> {
|
|||||||
|
|
||||||
List<NutMap> report(String activityId);
|
List<NutMap> report(String activityId);
|
||||||
|
|
||||||
|
List<NutMap> categoryReport(String activityId, String conditions);
|
||||||
|
|
||||||
|
NutMap crossReport(String activityId, String xSubjectIds, String ySubjectIds, String conditions);
|
||||||
|
|
||||||
|
NutMap compareReport(String activityId, String subjectId, String optionId);
|
||||||
|
|
||||||
|
List<NutMap> selectOptionUsers(String activityId, String subjectId, String optionId, String conditions);
|
||||||
|
|
||||||
void exportReportXlsx(String activityId, HttpServletResponse response);
|
void exportReportXlsx(String activityId, HttpServletResponse response);
|
||||||
|
|
||||||
void exportUserAnswerXlsx(String activityId, HttpServletResponse response);
|
void exportUserAnswerXlsx(String activityId, HttpServletResponse response);
|
||||||
|
|||||||
+8
@@ -17,6 +17,14 @@ public interface QsvUserAnswerRecordService extends BaseService<QsvUserAnswerRec
|
|||||||
*/
|
*/
|
||||||
float calcScore(QsvAnswerParam qsvAnswerParam);
|
float calcScore(QsvAnswerParam qsvAnswerParam);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验多选题提交答案是否超过题目配置的最大选择数,返回空表示校验通过。
|
||||||
|
*
|
||||||
|
* @param qsvAnswerParam 用户提交的答题参数
|
||||||
|
* @return 超限提示文案,空表示不超限
|
||||||
|
*/
|
||||||
|
String checkMaxMultiLimit(QsvAnswerParam qsvAnswerParam);
|
||||||
|
|
||||||
void calcByScoreMode(String activityId, String userId);
|
void calcByScoreMode(String activityId, String userId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+117
@@ -1,10 +1,23 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.qsv.service.impl;
|
package com.budwk.app.zhgh.dayofficework.qsv.service.impl;
|
||||||
|
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
|
import com.budwk.app.sys.models.Sys_home_activity;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.Strings;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
public class QsvActivityServiceImpl extends BaseServiceImpl<QsvActivity> implements QsvActivityService {
|
public class QsvActivityServiceImpl extends BaseServiceImpl<QsvActivity> implements QsvActivityService {
|
||||||
@@ -12,4 +25,108 @@ public class QsvActivityServiceImpl extends BaseServiceImpl<QsvActivity> impleme
|
|||||||
public QsvActivityServiceImpl(Dao dao) {
|
public QsvActivityServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public QsvActivity saveActivity(QsvActivity qsvActivity) {
|
||||||
|
return saveActivity(qsvActivity, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public QsvActivity saveActivity(QsvActivity qsvActivity, Boolean forceSave) {
|
||||||
|
NutMap confirmResult = checkAnswerModeChangeConfirm(qsvActivity);
|
||||||
|
if (!Boolean.TRUE.equals(forceSave) && Boolean.TRUE.equals(confirmResult.get("needConfirm"))) {
|
||||||
|
throw new IllegalStateException(confirmResult.getString("message"));
|
||||||
|
}
|
||||||
|
fillQuizRepeatMode(qsvActivity);
|
||||||
|
dao().insertOrUpdate(qsvActivity);
|
||||||
|
syncHomeActivity(qsvActivity);
|
||||||
|
return qsvActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NutMap checkAnswerModeChangeConfirm(QsvActivity qsvActivity) {
|
||||||
|
NutMap result = NutMap.NEW();
|
||||||
|
result.put("needConfirm", false);
|
||||||
|
result.put("message", "");
|
||||||
|
if (qsvActivity == null || Strings.isBlank(qsvActivity.getId())) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QsvActivity oldActivity = dao().fetch(QsvActivity.class, qsvActivity.getId());
|
||||||
|
if (oldActivity == null || !isAnswerModeChanged(oldActivity, qsvActivity)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int answerRecordCount = dao().count(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", qsvActivity.getId()));
|
||||||
|
if (answerRecordCount < 1) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Answer-mode changes affect unfinished answers, submitted answers and statistics, so the editor must confirm first.
|
||||||
|
result.put("needConfirm", true);
|
||||||
|
result.put("message", "\u5f53\u524d\u95ee\u5377\u5df2\u6709\u7528\u6237\u7b54\u9898\u4e2d\u6216\u5df2\u7b54\u9898\u5b8c\u6210\uff0c\u4fee\u6539\u7b54\u9898\u6a21\u5f0f\u53ef\u80fd\u5f71\u54cd\u73b0\u6709\u7b54\u9898\u8bb0\u5f55\u548c\u7edf\u8ba1\u7ed3\u679c\uff0c\u662f\u5426\u786e\u8ba4\u4fdd\u5b58\uff1f");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void deleteActivity(String id) {
|
||||||
|
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", id));
|
||||||
|
List<String> subjectIds = subjects.stream().map(QsvSubject::getId).collect(Collectors.toList());
|
||||||
|
if (!subjectIds.isEmpty()) {
|
||||||
|
dao().clear(QsvOption.class, Cnd.where("subjectId", "in", subjectIds));
|
||||||
|
dao().clear(QsvSubject.class, Cnd.where("id", "in", subjectIds));
|
||||||
|
}
|
||||||
|
dao().clear(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", id));
|
||||||
|
dao().delete(QsvActivity.class, id);
|
||||||
|
dao().delete(Sys_home_activity.class, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void updateEnabled(String id, Boolean enabled) {
|
||||||
|
QsvActivity activity = dao().fetch(QsvActivity.class, id);
|
||||||
|
if (activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activity.setEnabled(enabled);
|
||||||
|
dao().update(activity);
|
||||||
|
syncHomeActivity(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 答题活动的重复模式由出题模式决定。定时定题按天统计,常规模式按活动总次数统计。
|
||||||
|
*/
|
||||||
|
private void fillQuizRepeatMode(QsvActivity qsvActivity) {
|
||||||
|
if (!"QUIZ".equals(qsvActivity.getCategory())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("SCHEDULED".equals(qsvActivity.getMode())) {
|
||||||
|
qsvActivity.setRepeatMode("DAILY");
|
||||||
|
} else if ("REGULAR".equals(qsvActivity.getMode())) {
|
||||||
|
qsvActivity.setRepeatMode("TOTAL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断编辑前后的答题模式是否变化;问卷、答题等活动只要 mode 发生变化都需要参与风险确认。
|
||||||
|
*/
|
||||||
|
private boolean isAnswerModeChanged(QsvActivity oldActivity, QsvActivity newActivity) {
|
||||||
|
String oldMode = Strings.sNull(oldActivity.getMode()).trim();
|
||||||
|
String newMode = Strings.sNull(newActivity.getMode()).trim();
|
||||||
|
if (Strings.isBlank(oldMode) && Strings.isBlank(newMode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !Objects.equals(oldMode, newMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步移动端首页活动。问卷活动保存或启停后,都以活动自身 enabled、时间、人员范围生成首页记录。
|
||||||
|
*/
|
||||||
|
private void syncHomeActivity(QsvActivity activity) {
|
||||||
|
Sys_home_activity sysHomeActivity = activity.covertToSysHomeActivity();
|
||||||
|
dao().insertOrUpdate(sysHomeActivity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-9
@@ -90,6 +90,7 @@ public class QsvQuizRankServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord>
|
|||||||
private Sql buildQuerySql(String activityId, String mode, String scoreStatisticsMode, QsvQuizRankPageForm pageForm) {
|
private Sql buildQuerySql(String activityId, String mode, String scoreStatisticsMode, QsvQuizRankPageForm pageForm) {
|
||||||
StringBuilder sqlBuilder = new StringBuilder("""
|
StringBuilder sqlBuilder = new StringBuilder("""
|
||||||
SELECT
|
SELECT
|
||||||
|
t1.id,
|
||||||
t1.userId,
|
t1.userId,
|
||||||
t1.userName,
|
t1.userName,
|
||||||
t1.loginName,
|
t1.loginName,
|
||||||
@@ -119,16 +120,10 @@ public class QsvQuizRankServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord>
|
|||||||
cnd.andEX("t1.unionId","=",pageForm.getUnionId());
|
cnd.andEX("t1.unionId","=",pageForm.getUnionId());
|
||||||
cnd.andEX("t1.unitId","=",pageForm.getUnitId());
|
cnd.andEX("t1.unitId","=",pageForm.getUnitId());
|
||||||
|
|
||||||
if ("SCHEDULED".equals(mode)) {
|
if ("SCHEDULED".equals(mode) || "REGULAR".equals(mode)) {
|
||||||
if ("HIGH".equals(scoreStatisticsMode)) {
|
if ("HIGH".equals(scoreStatisticsMode) || "HIGHEST".equals(scoreStatisticsMode)) {
|
||||||
cnd.and("t1.isHighestScore", "=", 1);
|
cnd.and("t1.isHighestScore", "=", 1);
|
||||||
} else if ("LAST".equals(scoreStatisticsMode)) {
|
} else {
|
||||||
cnd.and("t1.isLatestScore","=",1);
|
|
||||||
}
|
|
||||||
} else if ("REGULAR".equals(mode)) {
|
|
||||||
if ("HIGH".equals(scoreStatisticsMode)) {
|
|
||||||
cnd.and("t1.isHighestScore","=",1);
|
|
||||||
} else if ("LAST".equals(scoreStatisticsMode)) {
|
|
||||||
cnd.and("t1.isLatestScore", "=", 1);
|
cnd.and("t1.isLatestScore", "=", 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+495
-40
@@ -8,6 +8,7 @@ import cn.afterturn.easypoi.excel.export.ExcelExportService;
|
|||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.hutool.json.JSONArray;
|
import cn.hutool.json.JSONArray;
|
||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
@@ -68,47 +69,47 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
// 处理每个题目
|
// 处理每个题目
|
||||||
for (NutMap subject : subjects) {
|
for (NutMap subject : subjects) {
|
||||||
// 获取当前题目的选项列表
|
// 获取当前题目的选项列表
|
||||||
List<NutMap> subjectOptions = Lang.collection2list(optionsGroup.get(subject.getString("id")), NutMap.class);
|
List<QsvOption> currentOptions = optionsGroup.get(subject.getString("id"));
|
||||||
|
List<NutMap> subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class);
|
||||||
|
|
||||||
// 获取题目类型
|
// 获取题目类型
|
||||||
String subjectType = subject.getString("type");
|
String subjectType = subject.getString("type");
|
||||||
|
|
||||||
// 处理文本类型题目
|
// 处理文本类型题目
|
||||||
if ("text".equals(subjectType)) {
|
if ("text".equals(subjectType)) {
|
||||||
List<String> texts = answerExtList.stream()
|
List<NutMap> textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id"));
|
||||||
.filter(ext -> ObjectUtil.isNull(ext.get(subject.getString("id"), JSONObject.class)))
|
List<String> texts = textAnswers.stream()
|
||||||
.map(ext -> ext.get(subject.getString("id"), JSONObject.class).getStr("text"))
|
.map(textAnswer -> textAnswer.getString("text"))
|
||||||
.toList();
|
.toList();
|
||||||
subject.put("texts", texts);
|
subject.put("texts", texts);
|
||||||
|
subject.put("textAnswers", textAnswers);
|
||||||
}
|
}
|
||||||
// 处理单选类型题目 处理多选类型题目
|
// 处理单选类型题目 处理多选类型题目
|
||||||
else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) {
|
else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) {
|
||||||
long selectTotal = answerExtList.stream()
|
long selectTotal = answerExtList.stream()
|
||||||
.filter(ext -> ext.containsKey(subject.getString("id")))
|
.filter(ext -> ObjectUtil.isNotEmpty(ext) && ext.containsKey(subject.getString("id")))
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
subjectOptions.forEach(subjectOption -> {
|
subjectOptions.forEach(subjectOption -> {
|
||||||
long selectCount = answerExtList.stream()
|
long selectCount = answerExtList.stream()
|
||||||
.filter(ext ->
|
.filter(ext ->
|
||||||
{
|
{
|
||||||
|
if (ObjectUtil.isEmpty(ext)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class);
|
JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class);
|
||||||
return jsonObject != null
|
return jsonObject != null
|
||||||
&& jsonObject.getJSONArray("optionIds") != null
|
&& jsonObject.getJSONArray("optionIds") != null
|
||||||
&& jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id"));
|
&& jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id"));
|
||||||
})
|
})
|
||||||
.count();
|
.count();
|
||||||
long round = Math.round((double) selectCount / selectTotal * 100);
|
long round = selectTotal == 0 ? 0 : Math.round((double) selectCount / selectTotal * 100);
|
||||||
subjectOption.put("selectPercent", round + "%");
|
subjectOption.put("selectPercent", round + "%");
|
||||||
subjectOption.put("selectCount", selectCount);
|
subjectOption.put("selectCount", selectCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
subjectOptions.sort((o1, o2) -> {
|
subjectOptions.sort((o1, o2) -> {
|
||||||
int count1 = o1.getInt("selectCount");
|
|
||||||
int count2 = o2.getInt("selectCount");
|
|
||||||
if (count1 == count2) {
|
|
||||||
return Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum"));
|
return Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum"));
|
||||||
}
|
|
||||||
return Integer.compare(count2, count1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
subject.put("selectTotal", selectTotal);
|
subject.put("selectTotal", selectTotal);
|
||||||
@@ -125,6 +126,228 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NutMap> categoryReport(String activityId, String conditions) {
|
||||||
|
if (activityId == null || activityId.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("activityId不能为空");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<NutMap> conditionList = parseCategoryConditions(conditions);
|
||||||
|
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
return buildReport(activityId, filterAnswerRecords(answerRecords, conditionList));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("分类报告生成失败", e);
|
||||||
|
throw new RuntimeException("分类报告生成失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NutMap crossReport(String activityId, String xSubjectIds, String ySubjectIds, String conditions) {
|
||||||
|
if (activityId == null || activityId.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("activityId不能为空");
|
||||||
|
}
|
||||||
|
List<String> xSubjectIdList = parseSubjectIds(xSubjectIds);
|
||||||
|
List<String> ySubjectIdList = parseSubjectIds(ySubjectIds);
|
||||||
|
if (ObjectUtil.isEmpty(xSubjectIdList) || ObjectUtil.isEmpty(ySubjectIdList)) {
|
||||||
|
throw new IllegalArgumentException("请选择自变量X和因变量Y");
|
||||||
|
}
|
||||||
|
if (xSubjectIdList.stream().anyMatch(ySubjectIdList::contains)) {
|
||||||
|
throw new IllegalArgumentException("自变量X和因变量Y不能选择相同变量");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<NutMap> subjects = querySubjects(activityId);
|
||||||
|
Map<String, NutMap> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(subject -> subject.getString("id"), subject -> subject));
|
||||||
|
if (!subjectMap.keySet().containsAll(xSubjectIdList) || !subjectMap.keySet().containsAll(ySubjectIdList)) {
|
||||||
|
throw new IllegalArgumentException("题目不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> allSubjectIds = new ArrayList<>();
|
||||||
|
allSubjectIds.addAll(xSubjectIdList);
|
||||||
|
allSubjectIds.addAll(ySubjectIdList);
|
||||||
|
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", allSubjectIds).asc("sortNum"));
|
||||||
|
Map<String, List<QsvOption>> optionGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
|
||||||
|
List<NutMap> xOptionMaps = buildVariableOptionCombinations(xSubjectIdList, subjectMap, optionGroup);
|
||||||
|
List<NutMap> conditionList = parseCategoryConditions(conditions);
|
||||||
|
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
List<QsvUserAnswerRecord> filteredRecords = filterAnswerRecords(answerRecords, conditionList);
|
||||||
|
List<NutMap> reports = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String ySubjectId : ySubjectIdList) {
|
||||||
|
NutMap ySubject = subjectMap.get(ySubjectId);
|
||||||
|
List<QsvOption> yOptions = optionGroup.get(ySubjectId);
|
||||||
|
if (ObjectUtil.isEmpty(yOptions)) {
|
||||||
|
yOptions = new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<NutMap> yOptionMaps = ObjectUtil.isEmpty(yOptions) ? new ArrayList<>() : Lang.collection2list(yOptions, NutMap.class);
|
||||||
|
List<NutMap> rows = new ArrayList<>();
|
||||||
|
|
||||||
|
for (NutMap xOption : xOptionMaps) {
|
||||||
|
List<NutMap> xConditions = xOption.getList("conditions", NutMap.class);
|
||||||
|
List<QsvUserAnswerRecord> xMatchedRecords = filteredRecords.stream()
|
||||||
|
.filter(record -> isRecordMatched(record, xConditions))
|
||||||
|
.toList();
|
||||||
|
long rowTotal = xMatchedRecords.size();
|
||||||
|
List<NutMap> cells = new ArrayList<>();
|
||||||
|
for (QsvOption yOption : yOptions) {
|
||||||
|
List<NutMap> yCondition = List.of(NutMap.NEW()
|
||||||
|
.addv("subjectId", ySubjectId)
|
||||||
|
.addv("optionId", yOption.getId()));
|
||||||
|
long count = xMatchedRecords.stream()
|
||||||
|
.filter(record -> isRecordMatched(record, yCondition))
|
||||||
|
.count();
|
||||||
|
double percent = rowTotal == 0 ? 0 : Math.round((double) count / rowTotal * 10000) / 100.0;
|
||||||
|
cells.add(NutMap.NEW()
|
||||||
|
.addv("yOptionId", yOption.getId())
|
||||||
|
.addv("yOptionText", yOption.getText())
|
||||||
|
.addv("count", count)
|
||||||
|
.addv("percent", percent));
|
||||||
|
}
|
||||||
|
rows.add(NutMap.NEW()
|
||||||
|
.addv("xOptionId", xOption.getString("id"))
|
||||||
|
.addv("xOptionText", xOption.getString("text"))
|
||||||
|
.addv("cells", cells)
|
||||||
|
.addv("total", rowTotal));
|
||||||
|
}
|
||||||
|
|
||||||
|
reports.add(NutMap.NEW()
|
||||||
|
.addv("ySubject", ySubject)
|
||||||
|
.addv("yOptions", yOptionMaps)
|
||||||
|
.addv("rows", rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("xSubject", buildVariableSubject(xSubjectIdList, subjectMap))
|
||||||
|
.addv("xOptions", xOptionMaps)
|
||||||
|
.addv("reports", reports)
|
||||||
|
.addv("total", filteredRecords.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("交叉分析报告生成失败", e);
|
||||||
|
throw new RuntimeException("交叉分析报告生成失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NutMap compareReport(String activityId, String subjectId, String optionId) {
|
||||||
|
if (activityId == null || activityId.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("activityId不能为空");
|
||||||
|
}
|
||||||
|
if (ObjectUtil.isEmpty(subjectId) || ObjectUtil.isEmpty(optionId)) {
|
||||||
|
throw new IllegalArgumentException("请选择对比变量和选项");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<NutMap> subjects = querySubjects(activityId);
|
||||||
|
Map<String, NutMap> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(subject -> subject.getString("id"), subject -> subject));
|
||||||
|
NutMap compareSubject = subjectMap.get(subjectId);
|
||||||
|
if (compareSubject == null) {
|
||||||
|
throw new IllegalArgumentException("对比变量不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> subjectIds = subjects.stream().map(subject -> subject.getString("id")).toList();
|
||||||
|
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
|
||||||
|
Map<String, List<QsvOption>> optionGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
|
||||||
|
QsvOption compareOption = optionGroup.getOrDefault(subjectId, new ArrayList<>()).stream()
|
||||||
|
.filter(option -> optionId.equals(option.getId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (compareOption == null) {
|
||||||
|
throw new IllegalArgumentException("对比选项不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<NutMap> compareCondition = List.of(NutMap.NEW()
|
||||||
|
.addv("subjectId", subjectId)
|
||||||
|
.addv("optionId", optionId));
|
||||||
|
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
List<QsvUserAnswerRecord> filteredRecords = filterAnswerRecords(answerRecords, compareCondition);
|
||||||
|
List<NutMap> reports = new ArrayList<>();
|
||||||
|
|
||||||
|
for (NutMap subject : subjects) {
|
||||||
|
String currentSubjectId = subject.getString("id");
|
||||||
|
String subjectType = subject.getString("type");
|
||||||
|
if (subjectId.equals(currentSubjectId) || (!"radio".equals(subjectType) && !"checkbox".equals(subjectType))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<QsvOption> currentOptions = optionGroup.get(currentSubjectId);
|
||||||
|
if (ObjectUtil.isEmpty(currentOptions)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<NutMap> cells = new ArrayList<>();
|
||||||
|
long rowTotal = filteredRecords.size();
|
||||||
|
for (QsvOption option : currentOptions) {
|
||||||
|
List<NutMap> optionCondition = List.of(NutMap.NEW()
|
||||||
|
.addv("subjectId", currentSubjectId)
|
||||||
|
.addv("optionId", option.getId()));
|
||||||
|
long count = filteredRecords.stream()
|
||||||
|
.filter(record -> isRecordMatched(record, optionCondition))
|
||||||
|
.count();
|
||||||
|
double percent = rowTotal == 0 ? 0 : Math.round((double) count / rowTotal * 10000) / 100.0;
|
||||||
|
cells.add(NutMap.NEW()
|
||||||
|
.addv("yOptionId", option.getId())
|
||||||
|
.addv("yOptionText", option.getText())
|
||||||
|
.addv("count", count)
|
||||||
|
.addv("percent", percent));
|
||||||
|
}
|
||||||
|
reports.add(NutMap.NEW()
|
||||||
|
.addv("ySubject", subject)
|
||||||
|
.addv("yOptions", Lang.collection2list(currentOptions, NutMap.class))
|
||||||
|
.addv("rows", List.of(NutMap.NEW()
|
||||||
|
.addv("xOptionId", optionId)
|
||||||
|
.addv("xOptionText", compareOption.getText())
|
||||||
|
.addv("cells", cells)
|
||||||
|
.addv("total", rowTotal))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("compareSubject", compareSubject)
|
||||||
|
.addv("compareOption", Lang.obj2nutmap(compareOption))
|
||||||
|
.addv("reports", reports)
|
||||||
|
.addv("total", filteredRecords.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("对比分析报告生成失败", e);
|
||||||
|
throw new RuntimeException("对比分析报告生成失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NutMap> selectOptionUsers(String activityId, String subjectId, String optionId, String conditions) {
|
||||||
|
List<NutMap> conditionList = parseCategoryConditions(conditions);
|
||||||
|
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
return filterAnswerRecords(answerRecords, conditionList).stream().map(record -> {
|
||||||
|
JSONObject extJson = record.getExtJson();
|
||||||
|
if (ObjectUtil.isEmpty(extJson)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject subjectAnswer = extJson.get(subjectId, JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(subjectAnswer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONArray optionIds = subjectAnswer.getJSONArray("optionIds");
|
||||||
|
if (ObjectUtil.isEmpty(optionIds) || !optionIds.contains(optionId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject optionFillContents = subjectAnswer.getJSONObject("optionFillContents");
|
||||||
|
String fillContent = ObjectUtil.isEmpty(optionFillContents) ? "" : optionFillContents.getStr(optionId);
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("userName", record.getUserName())
|
||||||
|
.addv("loginName", record.getLoginName())
|
||||||
|
.addv("unionName", record.getUnionName())
|
||||||
|
.addv("unitName", record.getUnitName())
|
||||||
|
.addv("attemptDate", record.getAttemptDate())
|
||||||
|
.addv("submitTime", record.getSubmitTime())
|
||||||
|
.addv("fillContent", fillContent);
|
||||||
|
})
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exportReportXlsx(String activityId, HttpServletResponse response) {
|
public void exportReportXlsx(String activityId, HttpServletResponse response) {
|
||||||
List<NutMap> report = report(activityId);
|
List<NutMap> report = report(activityId);
|
||||||
@@ -134,10 +357,8 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
exportEntities.add(new ExcelExportEntity("选择人数", "selectCount", 20));
|
exportEntities.add(new ExcelExportEntity("选择人数", "selectCount", 20));
|
||||||
exportEntities.add(new ExcelExportEntity("选择比例", "selectPercent", 20));
|
exportEntities.add(new ExcelExportEntity("选择比例", "selectPercent", 20));
|
||||||
|
|
||||||
Map<String, List<NutMap>> listMap = report.stream().collect(Collectors.groupingBy(v -> v.getString("id")));
|
|
||||||
Workbook workbook = new XSSFWorkbook();
|
Workbook workbook = new XSSFWorkbook();
|
||||||
listMap.forEach((k, v) -> {
|
report.forEach(nutMap -> {
|
||||||
NutMap nutMap = v.get(0);
|
|
||||||
ExcelExportService service = new ExcelExportService();
|
ExcelExportService service = new ExcelExportService();
|
||||||
ExportParams exportParams = new ExportParams();
|
ExportParams exportParams = new ExportParams();
|
||||||
exportParams.setTitle(nutMap.getString("title"));
|
exportParams.setTitle(nutMap.getString("title"));
|
||||||
@@ -171,7 +392,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
|
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
|
||||||
|
|
||||||
// 查询活动题目列表
|
// 查询活动题目列表
|
||||||
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
|
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
// 将题目列表转换为Map
|
// 将题目列表转换为Map
|
||||||
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
|
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
|
||||||
// 获取题目ID列表
|
// 获取题目ID列表
|
||||||
@@ -185,7 +406,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
// 构建Excel导出实体
|
// 构建Excel导出实体
|
||||||
List<ExcelExportEntity> excelExportEntities = buildExcelExportEntities(subjects);
|
List<ExcelExportEntity> excelExportEntities = buildExcelExportEntities(subjects);
|
||||||
// 构建答题记录列表
|
// 构建答题记录列表
|
||||||
List<NutMap> list = buildAnswerRecords(answerRecords, subjectMap, options);
|
List<NutMap> list = buildAnswerRecords(answerRecords, subjectMap, optionMap);
|
||||||
|
|
||||||
// 设置导出参数
|
// 设置导出参数
|
||||||
ExportParams exportParams = new ExportParams();
|
ExportParams exportParams = new ExportParams();
|
||||||
@@ -223,7 +444,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
// List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
|
// List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
|
||||||
|
|
||||||
// 查询活动题目列表
|
// 查询活动题目列表
|
||||||
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
|
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
// 将题目列表转换为Map
|
// 将题目列表转换为Map
|
||||||
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
|
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
|
||||||
// 获取题目ID列表
|
// 获取题目ID列表
|
||||||
@@ -245,18 +466,15 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
.addv("unitName", record.getString("unitName"))
|
.addv("unitName", record.getString("unitName"))
|
||||||
.addv("unionName", record.getString("unionName"));
|
.addv("unionName", record.getString("unionName"));
|
||||||
JSONObject extJson = record.getAs("extJson", JSONObject.class);
|
JSONObject extJson = record.getAs("extJson", JSONObject.class);
|
||||||
extJson.forEach((k, v) -> {
|
if (ObjectUtil.isEmpty(extJson)) {
|
||||||
JSONObject jsonVal = (JSONObject) v;
|
return map;
|
||||||
|
|
||||||
String type = subjectMap.get(k).getType();
|
|
||||||
if (type.equals("text")) {
|
|
||||||
map.addv(k, jsonVal.getStr("text"));
|
|
||||||
} else if (type.equals("radio") || type.equals("checkbox")) {
|
|
||||||
JSONArray optionIds = jsonVal.getJSONArray("optionIds");
|
|
||||||
String selectOptionTexts = options.stream().filter(option -> optionIds.contains(option.getId()))
|
|
||||||
.map(QsvOption::getText).collect(Collectors.joining(";"));
|
|
||||||
map.addv(k, selectOptionTexts);
|
|
||||||
}
|
}
|
||||||
|
extJson.forEach((k, v) -> {
|
||||||
|
QsvSubject subject = subjectMap.get(k);
|
||||||
|
if (subject == null || !(v instanceof JSONObject jsonVal)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.addv(k, buildAnswerText(jsonVal, subject, optionMap));
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}).toList();
|
}).toList();
|
||||||
@@ -278,12 +496,178 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<NutMap> buildReport(String activityId, List<QsvUserAnswerRecord> answerRecords) {
|
||||||
|
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
|
||||||
|
List<NutMap> subjects = querySubjects(activityId);
|
||||||
|
List<String> subjectIds = subjects.stream().map(v -> v.getString("id")).toList();
|
||||||
|
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
|
||||||
|
Map<String, List<QsvOption>> optionsGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
|
||||||
|
|
||||||
|
for (NutMap subject : subjects) {
|
||||||
|
List<QsvOption> currentOptions = optionsGroup.get(subject.getString("id"));
|
||||||
|
List<NutMap> subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class);
|
||||||
|
String subjectType = subject.getString("type");
|
||||||
|
if ("text".equals(subjectType)) {
|
||||||
|
List<NutMap> textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id"));
|
||||||
|
List<String> texts = textAnswers.stream()
|
||||||
|
.map(textAnswer -> textAnswer.getString("text"))
|
||||||
|
.toList();
|
||||||
|
subject.put("texts", texts);
|
||||||
|
subject.put("textAnswers", textAnswers);
|
||||||
|
} else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) {
|
||||||
|
long selectTotal = answerExtList.stream()
|
||||||
|
.filter(ext -> ObjectUtil.isNotEmpty(ext) && ext.containsKey(subject.getString("id")))
|
||||||
|
.count();
|
||||||
|
subjectOptions.forEach(subjectOption -> {
|
||||||
|
long selectCount = answerExtList.stream()
|
||||||
|
.filter(ext -> {
|
||||||
|
if (ObjectUtil.isEmpty(ext)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class);
|
||||||
|
return jsonObject != null
|
||||||
|
&& jsonObject.getJSONArray("optionIds") != null
|
||||||
|
&& jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id"));
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
long round = selectTotal == 0 ? 0 : Math.round((double) selectCount / selectTotal * 100);
|
||||||
|
subjectOption.put("selectPercent", round + "%");
|
||||||
|
subjectOption.put("selectCount", selectCount);
|
||||||
|
});
|
||||||
|
subjectOptions.sort((o1, o2) -> Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum")));
|
||||||
|
subject.put("selectTotal", selectTotal);
|
||||||
|
}
|
||||||
|
subject.addv("options", subjectOptions);
|
||||||
|
}
|
||||||
|
return subjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<NutMap> parseCategoryConditions(String conditions) {
|
||||||
|
if (ObjectUtil.isEmpty(conditions)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return JSONUtil.parseArray(conditions).toList(NutMap.class).stream()
|
||||||
|
.filter(condition -> ObjectUtil.isNotEmpty(condition.getString("subjectId"))
|
||||||
|
&& ObjectUtil.isNotEmpty(condition.getString("optionId")))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseSubjectIds(String subjectIds) {
|
||||||
|
if (ObjectUtil.isEmpty(subjectIds)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
String value = subjectIds.trim();
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
if (value.startsWith("[")) {
|
||||||
|
for (Object subjectId : JSONUtil.parseArray(value)) {
|
||||||
|
if (ObjectUtil.isNotEmpty(subjectId)) {
|
||||||
|
result.add(String.valueOf(subjectId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.stream().distinct().toList();
|
||||||
|
}
|
||||||
|
for (String subjectId : value.split(",")) {
|
||||||
|
if (ObjectUtil.isNotEmpty(subjectId)) {
|
||||||
|
result.add(subjectId.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.stream().distinct().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private NutMap buildVariableSubject(List<String> subjectIds, Map<String, NutMap> subjectMap) {
|
||||||
|
String title = subjectIds.stream()
|
||||||
|
.map(subjectId -> subjectMap.get(subjectId).getString("title"))
|
||||||
|
.collect(Collectors.joining(" / "));
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("id", String.join("|", subjectIds))
|
||||||
|
.addv("title", title);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<NutMap> buildVariableOptionCombinations(List<String> subjectIds, Map<String, NutMap> subjectMap, Map<String, List<QsvOption>> optionGroup) {
|
||||||
|
List<List<NutMap>> subjectOptionGroups = new ArrayList<>();
|
||||||
|
for (String subjectId : subjectIds) {
|
||||||
|
NutMap subject = subjectMap.get(subjectId);
|
||||||
|
List<QsvOption> options = optionGroup.get(subjectId);
|
||||||
|
if (ObjectUtil.isEmpty(options)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<NutMap> optionMaps = options.stream()
|
||||||
|
.map(option -> NutMap.NEW()
|
||||||
|
.addv("subjectId", subjectId)
|
||||||
|
.addv("subjectTitle", subject.getString("title"))
|
||||||
|
.addv("optionId", option.getId())
|
||||||
|
.addv("optionText", option.getText()))
|
||||||
|
.toList();
|
||||||
|
subjectOptionGroups.add(optionMaps);
|
||||||
|
}
|
||||||
|
if (subjectOptionGroups.size() != subjectIds.size()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<NutMap> result = new ArrayList<>();
|
||||||
|
buildVariableOptionCombinations(subjectOptionGroups, 0, new ArrayList<>(), result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildVariableOptionCombinations(List<List<NutMap>> subjectOptionGroups, int index, List<NutMap> current, List<NutMap> result) {
|
||||||
|
if (index >= subjectOptionGroups.size()) {
|
||||||
|
String id = current.stream()
|
||||||
|
.map(option -> option.getString("subjectId") + ":" + option.getString("optionId"))
|
||||||
|
.collect(Collectors.joining("|"));
|
||||||
|
String text = current.stream()
|
||||||
|
.map(option -> option.getString("optionText"))
|
||||||
|
.collect(Collectors.joining(" / "));
|
||||||
|
List<NutMap> conditions = current.stream()
|
||||||
|
.map(option -> NutMap.NEW()
|
||||||
|
.addv("subjectId", option.getString("subjectId"))
|
||||||
|
.addv("optionId", option.getString("optionId")))
|
||||||
|
.toList();
|
||||||
|
result.add(NutMap.NEW()
|
||||||
|
.addv("id", id)
|
||||||
|
.addv("text", text)
|
||||||
|
.addv("conditions", conditions));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (NutMap option : subjectOptionGroups.get(index)) {
|
||||||
|
current.add(option);
|
||||||
|
buildVariableOptionCombinations(subjectOptionGroups, index + 1, current, result);
|
||||||
|
current.remove(current.size() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QsvUserAnswerRecord> filterAnswerRecords(List<QsvUserAnswerRecord> answerRecords, List<NutMap> conditions) {
|
||||||
|
if (ObjectUtil.isEmpty(conditions)) {
|
||||||
|
return answerRecords;
|
||||||
|
}
|
||||||
|
return answerRecords.stream()
|
||||||
|
.filter(record -> isRecordMatched(record, conditions))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRecordMatched(QsvUserAnswerRecord record, List<NutMap> conditions) {
|
||||||
|
JSONObject extJson = record.getExtJson();
|
||||||
|
if (ObjectUtil.isEmpty(extJson)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (NutMap condition : conditions) {
|
||||||
|
JSONObject subjectAnswer = extJson.get(condition.getString("subjectId"), JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(subjectAnswer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JSONArray optionIds = subjectAnswer.getJSONArray("optionIds");
|
||||||
|
if (ObjectUtil.isEmpty(optionIds) || !optionIds.contains(condition.getString("optionId"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private List<NutMap> querySubjects(String activityId) {
|
private List<NutMap> querySubjects(String activityId) {
|
||||||
Sql subjectSql = Sqls.create("""
|
Sql subjectSql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
|
componentType,
|
||||||
sortNum
|
sortNum
|
||||||
FROM
|
FROM
|
||||||
qsv_subject
|
qsv_subject
|
||||||
@@ -309,7 +693,37 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
return excelExportEntities;
|
return excelExportEntities;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<NutMap> buildAnswerRecords(List<QsvUserAnswerRecord> answerRecords, Map<String, QsvSubject> subjectMap, List<QsvOption> options) {
|
/**
|
||||||
|
* 构建填空题填写详情。分析弹窗需要同时展示填写内容和答题人信息,
|
||||||
|
* 因此这里从已完成答题记录中提取当前题目的文本答案,并携带姓名、工号、所属工会和单位。
|
||||||
|
*
|
||||||
|
* @param answerRecords 当前活动已完成的答题记录
|
||||||
|
* @param subjectId 当前填空题ID,用于从 extJson 中取出该题填写内容
|
||||||
|
* @return 填空题填写详情列表,每条包含 loginName、userName、unionName、unitName、submitTime、text
|
||||||
|
*/
|
||||||
|
private List<NutMap> buildTextAnswerDetails(List<QsvUserAnswerRecord> answerRecords, String subjectId) {
|
||||||
|
List<NutMap> textAnswers = new ArrayList<>();
|
||||||
|
for (QsvUserAnswerRecord answerRecord : answerRecords) {
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (ObjectUtil.isEmpty(extJson)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JSONObject subjectAnswer = extJson.get(subjectId, JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(subjectAnswer) || ObjectUtil.isEmpty(subjectAnswer.getStr("text"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
textAnswers.add(NutMap.NEW()
|
||||||
|
.addv("loginName", answerRecord.getLoginName())
|
||||||
|
.addv("userName", answerRecord.getUserName())
|
||||||
|
.addv("unionName", answerRecord.getUnionName())
|
||||||
|
.addv("unitName", answerRecord.getUnitName())
|
||||||
|
.addv("submitTime", answerRecord.getSubmitTime())
|
||||||
|
.addv("text", subjectAnswer.getStr("text")));
|
||||||
|
}
|
||||||
|
return textAnswers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<NutMap> buildAnswerRecords(List<QsvUserAnswerRecord> answerRecords, Map<String, QsvSubject> subjectMap, Map<String, QsvOption> optionMap) {
|
||||||
return answerRecords.stream().map(record -> {
|
return answerRecords.stream().map(record -> {
|
||||||
NutMap map = NutMap.NEW()
|
NutMap map = NutMap.NEW()
|
||||||
.addv("id", record.getId())
|
.addv("id", record.getId())
|
||||||
@@ -318,20 +732,61 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
.addv("unitName", record.getUnitName())
|
.addv("unitName", record.getUnitName())
|
||||||
.addv("unionName", record.getUnionName());
|
.addv("unionName", record.getUnionName());
|
||||||
JSONObject extJson = record.getExtJson();
|
JSONObject extJson = record.getExtJson();
|
||||||
extJson.forEach((k, v) -> {
|
if (ObjectUtil.isEmpty(extJson)) {
|
||||||
JSONObject jsonVal = (JSONObject) v;
|
return map;
|
||||||
|
|
||||||
String type = subjectMap.get(k).getType();
|
|
||||||
if (type.equals("text")) {
|
|
||||||
map.addv(k, jsonVal.getStr("text"));
|
|
||||||
} else if (type.equals("radio") || type.equals("checkbox")) {
|
|
||||||
JSONArray optionIds = jsonVal.getJSONArray("optionIds");
|
|
||||||
String selectOptionTexts = options.stream().filter(option -> optionIds.contains(option.getId()))
|
|
||||||
.map(QsvOption::getText).collect(Collectors.joining(";"));
|
|
||||||
map.addv(k, selectOptionTexts);
|
|
||||||
}
|
}
|
||||||
|
extJson.forEach((k, v) -> {
|
||||||
|
QsvSubject subject = subjectMap.get(k);
|
||||||
|
if (subject == null || !(v instanceof JSONObject jsonVal)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.addv(k, buildAnswerText(jsonVal, subject, optionMap));
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组装单题答案展示内容。填空题返回填写文本;选择题返回选项文本,若选项配置了补充填写,
|
||||||
|
* 则追加对应补充内容,保证后台查看答卷和导出能看到用户填写的说明。
|
||||||
|
*
|
||||||
|
* @param jsonVal 答题记录 extJson 中当前题目的答案对象,包含 optionIds、text、optionFillContents
|
||||||
|
* @param subject 当前题目,用于判断题目类型
|
||||||
|
* @param optionMap 当前活动所有选项,key 为选项ID,value 为选项实体
|
||||||
|
* @return 当前题目的答案展示文本
|
||||||
|
*/
|
||||||
|
private String buildAnswerText(JSONObject jsonVal, QsvSubject subject, Map<String, QsvOption> optionMap) {
|
||||||
|
String type = subject.getType();
|
||||||
|
if ("text".equals(type)) {
|
||||||
|
return jsonVal.getStr("text");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"radio".equals(type) && !"checkbox".equals(type)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray optionIds = jsonVal.getJSONArray("optionIds");
|
||||||
|
if (ObjectUtil.isEmpty(optionIds)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject optionFillContents = jsonVal.getJSONObject("optionFillContents");
|
||||||
|
List<String> answerTexts = new ArrayList<>();
|
||||||
|
for (Object optionIdObj : optionIds) {
|
||||||
|
String optionId = String.valueOf(optionIdObj);
|
||||||
|
QsvOption option = optionMap.get(optionId);
|
||||||
|
if (option == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String answerText = option.getText();
|
||||||
|
if (ObjectUtil.isNotEmpty(optionFillContents)) {
|
||||||
|
String fillContent = optionFillContents.getStr(optionId);
|
||||||
|
if (ObjectUtil.isNotEmpty(fillContent)) {
|
||||||
|
answerText = answerText + ":" + fillContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
answerTexts.add(answerText);
|
||||||
|
}
|
||||||
|
return String.join(";", answerTexts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-2
@@ -26,6 +26,44 @@ public class QsvUserAnswerRecordServiceImpl extends BaseServiceImpl<QsvUserAnswe
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按题目配置校验多选题最大选择数,防止前端限制被绕过后写入超量选项。
|
||||||
|
*
|
||||||
|
* @param qsvAnswerParam 用户提交的答题参数
|
||||||
|
* @return 超限提示文案,空表示校验通过
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String checkMaxMultiLimit(QsvAnswerParam qsvAnswerParam) {
|
||||||
|
if (qsvAnswerParam == null || qsvAnswerParam.getSubjects() == null || qsvAnswerParam.getSubjects().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<String> subjectIds = qsvAnswerParam.getSubjects().stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(QsvAnswerParam.Subject::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
if (subjectIds.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<String, QsvSubject> subjectMap = new HashMap<>();
|
||||||
|
dao().query(QsvSubject.class, Cnd.where("id", "in", subjectIds)).forEach(subject -> subjectMap.put(subject.getId(), subject));
|
||||||
|
for (QsvAnswerParam.Subject answerSubject : qsvAnswerParam.getSubjects()) {
|
||||||
|
if (answerSubject == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QsvSubject configSubject = subjectMap.get(answerSubject.getId());
|
||||||
|
if (configSubject == null || !"checkbox".equals(configSubject.getType()) || configSubject.getMaxMulti() == null || configSubject.getMaxMulti() <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> selectedOptionIds = answerSubject.getUserSelectOptionIds();
|
||||||
|
int selectedCount = selectedOptionIds == null ? 0 : new LinkedHashSet<>(selectedOptionIds).size();
|
||||||
|
if (selectedCount > configSubject.getMaxMulti()) {
|
||||||
|
return "题目“" + configSubject.getTitle() + "”最多选择" + configSubject.getMaxMulti() + "项";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public QsvUserAnswerRecord insertRecord(String activityId, List<String> subjectIds) {
|
public QsvUserAnswerRecord insertRecord(String activityId, List<String> subjectIds) {
|
||||||
QsvActivity activity = dao().fetch(QsvActivity.class, activityId);
|
QsvActivity activity = dao().fetch(QsvActivity.class, activityId);
|
||||||
@@ -121,11 +159,12 @@ public class QsvUserAnswerRecordServiceImpl extends BaseServiceImpl<QsvUserAnswe
|
|||||||
|
|
||||||
String mode = activity.getMode();
|
String mode = activity.getMode();
|
||||||
String scoreMode = activity.getScoreMode();
|
String scoreMode = activity.getScoreMode();
|
||||||
|
boolean highestScoreMode = "HIGH".equals(scoreMode) || "HIGHEST".equals(scoreMode);
|
||||||
|
|
||||||
if (mode.equals("SCHEDULED")) {
|
if (mode.equals("SCHEDULED")) {
|
||||||
processScoreRecords(activityId, SecurityUtil.getUserId(), true, scoreMode.equals("HIGH"));
|
processScoreRecords(activityId, SecurityUtil.getUserId(), true, highestScoreMode);
|
||||||
} else if (mode.equals("REGULAR")) {
|
} else if (mode.equals("REGULAR")) {
|
||||||
processScoreRecords(activityId, SecurityUtil.getUserId(), false, scoreMode.equals("HIGH"));
|
processScoreRecords(activityId, SecurityUtil.getUserId(), false, highestScoreMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+202
-23
@@ -1,6 +1,7 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.tour.controller;
|
package com.budwk.app.zhgh.dayofficework.tour.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.annotation.SaMode;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
@@ -169,58 +170,184 @@ public class TourGroupController {
|
|||||||
return Result.success(sql.getList(NutMap.class));
|
return Result.success(sql.getList(NutMap.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前事项线路下已经写入疗休养台账的成团信息,供线路成团查看页展示团标签。
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("tour.group")
|
@SaCheckPermission("tour.group")
|
||||||
public Result signupPageData(PageForm pageForm, String matterId, String keyword, String unionId) {
|
public Result groupOptions(String matterId) {
|
||||||
|
if (StrUtil.isBlank(matterId)) {
|
||||||
|
return Result.error("参数错误");
|
||||||
|
}
|
||||||
|
Cnd cnd = buildGroupOptionCnd(matterId);
|
||||||
|
return Result.success(tourLedgerService.queryGroupOptions(cnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动将当前事项线路下未成团的报名人员按最大成团人数分配成团。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("tour.group")
|
||||||
|
public Result assignGroup(String matterId) {
|
||||||
|
if (StrUtil.isBlank(matterId)) {
|
||||||
|
return Result.error("参数错误");
|
||||||
|
}
|
||||||
|
return tourLedgerService.assignGroup(matterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将勾选的教职工报名台账调整到指定成团中,家属随教职工一起调整。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("tour.group")
|
||||||
|
public Result adjustGroup(String matterId, String groupCode, String ledgerIds) {
|
||||||
|
if (StrUtil.isBlank(matterId) || StrUtil.isBlank(groupCode) || StrUtil.isBlank(ledgerIds)) {
|
||||||
|
return Result.error("参数错误");
|
||||||
|
}
|
||||||
|
return tourLedgerService.adjustGroup(matterId, groupCode, ledgerIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission(value = {"tour.group", "tour.signup"}, mode = SaMode.OR)
|
||||||
|
public Result signupPageData(PageForm pageForm, String matterId, String keyword, String unionId, String groupCode) {
|
||||||
if (StrUtil.isBlank(matterId)) {
|
if (StrUtil.isBlank(matterId)) {
|
||||||
return Result.error("参数错误");
|
return Result.error("参数错误");
|
||||||
}
|
}
|
||||||
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
||||||
pageForm.defaultSortDesc("signupTime");
|
pageForm.defaultSortAsc("groupCode");
|
||||||
}
|
}
|
||||||
Cnd cnd = buildSignupQueryCnd(matterId, keyword, unionId);
|
Cnd cnd = buildSignupQueryCnd(matterId, keyword, unionId, groupCode);
|
||||||
|
boolean maskOtherUnionPeople = shouldMaskOtherUnionSignupPeople();
|
||||||
|
String currentUnionId = StrUtil.blankToDefault(SecurityUtil.getUnionId(), "");
|
||||||
Sql countSql = Sqls.create("""
|
Sql countSql = Sqls.create("""
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
|
FROM (
|
||||||
|
SELECT t.id AS rowId
|
||||||
FROM tour_ledger t
|
FROM tour_ledger t
|
||||||
INNER JOIN tour_matter m ON m.id = t.matterId
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
INNER JOIN tour_line l ON l.id = m.lineId
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
$condition
|
$condition
|
||||||
|
UNION ALL
|
||||||
|
SELECT f.id AS rowId
|
||||||
|
FROM tour_ledger t
|
||||||
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
INNER JOIN tour_ledger_family f ON f.ledgerId = t.id AND f.delFlag = 0
|
||||||
|
$condition
|
||||||
|
AND (@maskOtherUnionPeople = 0 OR IFNULL(t.unionId, '') = @currentUnionId)
|
||||||
|
) p
|
||||||
""");
|
""");
|
||||||
countSql.setCondition(cnd);
|
countSql.setCondition(cnd);
|
||||||
|
countSql.setParam("currentUnionId", currentUnionId);
|
||||||
|
countSql.setParam("maskOtherUnionPeople", maskOtherUnionPeople ? 1 : 0);
|
||||||
countSql.setCallback(Sqls.callback.integer());
|
countSql.setCallback(Sqls.callback.integer());
|
||||||
tourLedgerService.dao().execute(countSql);
|
tourLedgerService.dao().execute(countSql);
|
||||||
int count = countSql.getInt();
|
int count = countSql.getInt();
|
||||||
|
|
||||||
Sql listSql = Sqls.create("""
|
Sql listSql = Sqls.create("""
|
||||||
|
SELECT *
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
|
t.id AS ledgerId,
|
||||||
t.jobNo,
|
t.jobNo,
|
||||||
t.userName,
|
t.userName,
|
||||||
|
t.gender,
|
||||||
'' AS mobile,
|
'' AS mobile,
|
||||||
t.idCard,
|
CASE WHEN @maskOtherUnionPeople = 1 AND IFNULL(t.unionId, '') <> @currentUnionId THEN '' ELSE t.idCard END AS idCard,
|
||||||
t.unionId,
|
t.unionId,
|
||||||
t.unionName,
|
t.unionName,
|
||||||
t.signupTime
|
t.signupTime,
|
||||||
|
t.groupCode,
|
||||||
|
t.groupName,
|
||||||
|
CASE WHEN @maskOtherUnionPeople = 1 AND IFNULL(t.unionId, '') <> @currentUnionId THEN 0 ELSE IFNULL(fc.familyCount, 0) END AS familyCount,
|
||||||
|
CASE WHEN @maskOtherUnionPeople = 1 AND IFNULL(t.unionId, '') <> @currentUnionId THEN 1 ELSE 1 + IFNULL(fc.familyCount, 0) END AS totalPeople,
|
||||||
|
1 AS staffRow,
|
||||||
|
1 AS selectable,
|
||||||
|
'教职工' AS rowTypeName,
|
||||||
|
'' AS relationship,
|
||||||
|
t.userName AS relatedStaffName,
|
||||||
|
0 AS rowSort,
|
||||||
|
t.createdAt AS ledgerCreatedAt,
|
||||||
|
t.createdAt
|
||||||
FROM tour_ledger t
|
FROM tour_ledger t
|
||||||
INNER JOIN tour_matter m ON m.id = t.matterId
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
INNER JOIN tour_line l ON l.id = m.lineId
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
|
FROM tour_ledger_family
|
||||||
|
WHERE delFlag = 0
|
||||||
|
GROUP BY ledgerId
|
||||||
|
) fc ON fc.ledgerId = t.id
|
||||||
$condition
|
$condition
|
||||||
ORDER BY $orderColumn $orderBy, t.signupTime DESC, t.createdAt DESC
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
f.id,
|
||||||
|
t.id AS ledgerId,
|
||||||
|
'' AS jobNo,
|
||||||
|
f.familyName AS userName,
|
||||||
|
f.gender,
|
||||||
|
'' AS mobile,
|
||||||
|
f.idCard,
|
||||||
|
t.unionId,
|
||||||
|
'' AS unionName,
|
||||||
|
t.signupTime,
|
||||||
|
t.groupCode,
|
||||||
|
t.groupName,
|
||||||
|
0 AS familyCount,
|
||||||
|
'' AS totalPeople,
|
||||||
|
0 AS staffRow,
|
||||||
|
0 AS selectable,
|
||||||
|
'家属' AS rowTypeName,
|
||||||
|
f.relationship,
|
||||||
|
t.userName AS relatedStaffName,
|
||||||
|
1 AS rowSort,
|
||||||
|
t.createdAt AS ledgerCreatedAt,
|
||||||
|
f.createdAt
|
||||||
|
FROM tour_ledger t
|
||||||
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
INNER JOIN tour_ledger_family f ON f.ledgerId = t.id AND f.delFlag = 0
|
||||||
|
$condition
|
||||||
|
AND (@maskOtherUnionPeople = 0 OR IFNULL(t.unionId, '') = @currentUnionId)
|
||||||
|
) p
|
||||||
|
ORDER BY $orderColumn $orderBy, p.signupTime ASC, p.ledgerCreatedAt ASC, p.rowSort ASC, p.createdAt ASC
|
||||||
""");
|
""");
|
||||||
listSql.setCondition(cnd);
|
listSql.setCondition(cnd);
|
||||||
listSql.setVar("orderColumn", getSignupOrderColumn(pageForm.getPageOrderName()));
|
listSql.setParam("currentUnionId", currentUnionId);
|
||||||
|
listSql.setParam("maskOtherUnionPeople", maskOtherUnionPeople ? 1 : 0);
|
||||||
|
listSql.setVar("orderColumn", getSignupUnionOrderColumn(pageForm.getPageOrderName()));
|
||||||
listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc");
|
listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc");
|
||||||
listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||||
listSql.setCallback(Sqls.callback.maps());
|
listSql.setCallback(Sqls.callback.maps());
|
||||||
tourLedgerService.dao().execute(listSql);
|
tourLedgerService.dao().execute(listSql);
|
||||||
|
|
||||||
List<NutMap> list = listSql.getList(NutMap.class);
|
List<NutMap> list = listSql.getList(NutMap.class);
|
||||||
fillSignupMobile(list);
|
// 人员列表已在 SQL 层按教职工和家属平铺后分页,这里只补齐教职工手机号,避免分页后再次追加家属行。
|
||||||
fillSignupFamilies(list);
|
fillSignupMobile(list, maskOtherUnionPeople);
|
||||||
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
|
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
|
||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造成团标签查询条件:只取当前事项线路已分配成团编码的台账,并保持与列表一致的数据权限。
|
||||||
|
*/
|
||||||
|
private Cnd buildGroupOptionCnd(String matterId) {
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
cnd.and("t.delFlag", "=", false);
|
||||||
|
cnd.and("t.matterId", "=", matterId);
|
||||||
|
cnd.and("t.groupCode", "IS NOT", null);
|
||||||
|
cnd.and("t.groupCode", "<>", "");
|
||||||
|
cnd.and("m.delFlag", "=", false);
|
||||||
|
cnd.and("m.enabled", "=", true);
|
||||||
|
cnd.and("m.lineId", "IS NOT", null);
|
||||||
|
cnd.and("m.lineId", "<>", "");
|
||||||
|
cnd.and("l.delFlag", "=", false);
|
||||||
|
cnd.and("l.enabled", "=", true);
|
||||||
|
applyMatterDataScope(cnd);
|
||||||
|
return cnd;
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@Ok("void")
|
@Ok("void")
|
||||||
@SaCheckPermission("tour.group")
|
@SaCheckPermission("tour.group")
|
||||||
@@ -238,7 +365,7 @@ public class TourGroupController {
|
|||||||
CommonDownloadUtil.download(lineName + "参加人员名单.xls", workbook, response);
|
CommonDownloadUtil.download(lineName + "参加人员名单.xls", workbook, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cnd buildSignupQueryCnd(String matterId, String keyword, String unionId) {
|
private Cnd buildSignupQueryCnd(String matterId, String keyword, String unionId, String groupCode) {
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
cnd.and("t.delFlag", "=", false);
|
cnd.and("t.delFlag", "=", false);
|
||||||
cnd.and("t.matterId", "=", matterId);
|
cnd.and("t.matterId", "=", matterId);
|
||||||
@@ -255,7 +382,8 @@ public class TourGroupController {
|
|||||||
cnd.and(keywordGroup);
|
cnd.and(keywordGroup);
|
||||||
}
|
}
|
||||||
cnd.andEX("t.unionId", "=", unionId);
|
cnd.andEX("t.unionId", "=", unionId);
|
||||||
applyMatterDataScope(cnd);
|
// 团标签联动查询时只筛选当前团;未选择团标签时默认查询全部报名人员。
|
||||||
|
cnd.andEX("t.groupCode", "=", groupCode);
|
||||||
return cnd;
|
return cnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +425,7 @@ public class TourGroupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<NutMap> queryExportParticipants(String matterId) {
|
private List<NutMap> queryExportParticipants(String matterId) {
|
||||||
Cnd cnd = buildSignupQueryCnd(matterId, null, null);
|
Cnd cnd = buildSignupQueryCnd(matterId, null, null, null);
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
t.id AS ledgerId,
|
t.id AS ledgerId,
|
||||||
@@ -485,12 +613,16 @@ public class TourGroupController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fillSignupMobile(List<NutMap> list) {
|
private void fillSignupMobile(List<NutMap> list, boolean maskOtherUnionPeople) {
|
||||||
if (list == null || list.isEmpty()) {
|
if (list == null || list.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<String> jobNos = new ArrayList<>();
|
List<String> jobNos = new ArrayList<>();
|
||||||
|
String currentUnionId = StrUtil.blankToDefault(SecurityUtil.getUnionId(), "");
|
||||||
for (NutMap item : list) {
|
for (NutMap item : list) {
|
||||||
|
if (maskOtherUnionPeople && !currentUnionId.equals(item.getString("unionId", ""))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
String jobNo = item.getString("jobNo", "");
|
String jobNo = item.getString("jobNo", "");
|
||||||
if (StrUtil.isNotBlank(jobNo) && !jobNos.contains(jobNo)) {
|
if (StrUtil.isNotBlank(jobNo) && !jobNos.contains(jobNo)) {
|
||||||
jobNos.add(jobNo);
|
jobNos.add(jobNo);
|
||||||
@@ -510,14 +642,18 @@ public class TourGroupController {
|
|||||||
mobileMap.put(user.getString("loginname", ""), user.getString("mobile", ""));
|
mobileMap.put(user.getString("loginname", ""), user.getString("mobile", ""));
|
||||||
}
|
}
|
||||||
for (NutMap item : list) {
|
for (NutMap item : list) {
|
||||||
|
if (maskOtherUnionPeople && !currentUnionId.equals(item.getString("unionId", ""))) {
|
||||||
|
item.put("mobile", "");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
item.put("mobile", mobileMap.getOrDefault(item.getString("jobNo", ""), ""));
|
item.put("mobile", mobileMap.getOrDefault(item.getString("jobNo", ""), ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查看成团报名人员时按台账ID带出家属子列表,避免前端逐行请求家属信息。
|
* 查看成团报名人员时将教职工和家属平铺在同一个列表,便于前端统一展示。
|
||||||
*/
|
*/
|
||||||
private void fillSignupFamilies(List<NutMap> list) {
|
private void flattenSignupFamilies(List<NutMap> list) {
|
||||||
if (list == null || list.isEmpty()) {
|
if (list == null || list.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -535,8 +671,9 @@ public class TourGroupController {
|
|||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
ledgerId,
|
ledgerId,
|
||||||
staffJobNo AS jobNo,
|
'' AS jobNo,
|
||||||
familyName AS userName,
|
familyName AS userName,
|
||||||
|
gender,
|
||||||
'' AS mobile,
|
'' AS mobile,
|
||||||
idCard,
|
idCard,
|
||||||
relationship
|
relationship
|
||||||
@@ -551,11 +688,35 @@ public class TourGroupController {
|
|||||||
|
|
||||||
Map<String, List<NutMap>> familyMap = new HashMap<>();
|
Map<String, List<NutMap>> familyMap = new HashMap<>();
|
||||||
for (NutMap family : sql.getList(NutMap.class)) {
|
for (NutMap family : sql.getList(NutMap.class)) {
|
||||||
|
family.put("staffRow", false);
|
||||||
|
family.put("selectable", false);
|
||||||
|
family.put("rowTypeName", "家属");
|
||||||
|
family.put("unionName", "");
|
||||||
|
family.put("groupCode", "");
|
||||||
|
family.put("groupName", "");
|
||||||
|
family.put("totalPeople", "");
|
||||||
familyMap.computeIfAbsent(family.getString("ledgerId", ""), key -> new ArrayList<>()).add(family);
|
familyMap.computeIfAbsent(family.getString("ledgerId", ""), key -> new ArrayList<>()).add(family);
|
||||||
}
|
}
|
||||||
|
List<NutMap> flatList = new ArrayList<>();
|
||||||
for (NutMap item : list) {
|
for (NutMap item : list) {
|
||||||
item.put("families", familyMap.getOrDefault(item.getString("id", ""), List.of()));
|
// 教职工是后续人员调整的最小可选单位,家属随教职工一起分配。
|
||||||
|
item.put("staffRow", true);
|
||||||
|
item.put("selectable", true);
|
||||||
|
item.put("rowTypeName", "教职工");
|
||||||
|
item.put("relationship", "");
|
||||||
|
item.put("relatedStaffName", item.getString("userName", ""));
|
||||||
|
flatList.add(item);
|
||||||
|
List<NutMap> families = familyMap.getOrDefault(item.getString("id", ""), List.of());
|
||||||
|
for (NutMap family : families) {
|
||||||
|
// 家属行通过关联教职工名称标明所属教职工,便于单层列表中识别家庭关系。
|
||||||
|
family.put("relatedStaffName", item.getString("userName", ""));
|
||||||
|
family.put("groupCode", item.getString("groupCode", ""));
|
||||||
|
family.put("groupName", item.getString("groupName", ""));
|
||||||
}
|
}
|
||||||
|
flatList.addAll(families);
|
||||||
|
}
|
||||||
|
list.clear();
|
||||||
|
list.addAll(flatList);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId) {
|
private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId) {
|
||||||
@@ -606,6 +767,15 @@ public class TourGroupController {
|
|||||||
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Branch union chairmen can browse all staff rows, but sensitive fields and family rows outside their union are hidden.
|
||||||
|
*/
|
||||||
|
private boolean shouldMaskOtherUnionSignupPeople() {
|
||||||
|
return AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|
||||||
|
&& !AuthUtil.hasRole(RoleConstant.SYSADMIN.name())
|
||||||
|
&& !AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name());
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasBranchUnionScope() {
|
private boolean hasBranchUnionScope() {
|
||||||
return AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name())
|
return AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name())
|
||||||
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|
||||||
@@ -655,19 +825,28 @@ public class TourGroupController {
|
|||||||
return "l.lineName";
|
return "l.lineName";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getSignupOrderColumn(String orderName) {
|
private String getSignupUnionOrderColumn(String orderName) {
|
||||||
if ("jobNo".equals(orderName)) {
|
if ("jobNo".equals(orderName)) {
|
||||||
return "t.jobNo";
|
return "p.jobNo";
|
||||||
}
|
}
|
||||||
if ("userName".equals(orderName)) {
|
if ("userName".equals(orderName)) {
|
||||||
return "t.userName";
|
return "p.userName";
|
||||||
|
}
|
||||||
|
if ("gender".equals(orderName)) {
|
||||||
|
return "p.gender";
|
||||||
}
|
}
|
||||||
if ("idCard".equals(orderName)) {
|
if ("idCard".equals(orderName)) {
|
||||||
return "t.idCard";
|
return "p.idCard";
|
||||||
}
|
}
|
||||||
if ("unionName".equals(orderName)) {
|
if ("unionName".equals(orderName)) {
|
||||||
return "t.unionName";
|
return "p.unionName";
|
||||||
}
|
}
|
||||||
return "t.signupTime";
|
if ("groupCode".equals(orderName) || "groupName".equals(orderName)) {
|
||||||
|
return "p.groupCode";
|
||||||
|
}
|
||||||
|
if ("totalPeople".equals(orderName)) {
|
||||||
|
return "p.totalPeople";
|
||||||
|
}
|
||||||
|
return "p.signupTime";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+71
@@ -26,6 +26,7 @@ import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency;
|
|||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
|
||||||
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||||
import org.apache.poi.ss.usermodel.BorderStyle;
|
import org.apache.poi.ss.usermodel.BorderStyle;
|
||||||
@@ -90,6 +91,9 @@ public class TourLedgerController {
|
|||||||
@Inject
|
@Inject
|
||||||
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
|
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private TourParticipantRosterService tourParticipantRosterService;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("beetl:/platform/zhgh/dayofficework/Tour/ledger/index.html")
|
@Ok("beetl:/platform/zhgh/dayofficework/Tour/ledger/index.html")
|
||||||
@SaCheckPermission("tour.ledger")
|
@SaCheckPermission("tour.ledger")
|
||||||
@@ -216,6 +220,8 @@ public class TourLedgerController {
|
|||||||
if (ledger == null) {
|
if (ledger == null) {
|
||||||
return Result.error("台账记录不存在");
|
return Result.error("台账记录不存在");
|
||||||
}
|
}
|
||||||
|
// Reset the participant roster before deleting the ledger record from the admin ledger page.
|
||||||
|
tourParticipantRosterService.markNotJoinedByLedger(ledger);
|
||||||
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", id));
|
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", id));
|
||||||
tourLedgerService.delete(id);
|
tourLedgerService.delete(id);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
@@ -313,6 +319,59 @@ public class TourLedgerController {
|
|||||||
CommonDownloadUtil.download("参加人员导入模板.xlsx", workbook, response);
|
CommonDownloadUtil.download("参加人员导入模板.xlsx", workbook, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按台账页面当前查询条件导出全部匹配记录。
|
||||||
|
*
|
||||||
|
* @param startYear 开始年度,传四位年度数字
|
||||||
|
* @param endYear 结束年度,传四位年度数字
|
||||||
|
* @param keyword 姓名或工号关键字,可为空
|
||||||
|
* @param unionId 所属工会ID,可为空
|
||||||
|
* @param lineId 线路ID;历史线路使用legacy:线路名称,可为空
|
||||||
|
* @param travelPeriod 出行时段,格式为开始时间 至 结束时间,可为空
|
||||||
|
* @param lineType 线路类型字典名称,可为空
|
||||||
|
* @param directFamilyOnly 是否仅导出参加直系亲属单位线路人员
|
||||||
|
* @param overCostOnly 是否仅导出申请超出费用由单位承担人员
|
||||||
|
* @param response HTTP响应,返回XSSF格式的Excel文件流
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@SaCheckPermission("tour.ledger")
|
||||||
|
public void exportData(Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
|
||||||
|
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType,
|
||||||
|
directFamilyOnly, overCostOnly);
|
||||||
|
List<NutMap> rows = tourLedgerService.queryExportRows(cnd);
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
NutMap row = rows.get(i);
|
||||||
|
row.put("index", i + 1);
|
||||||
|
row.put("groupInfo", formatGroupInfo(row));
|
||||||
|
row.put("joinedName", row.getBoolean("joined", false) ? "是" : "否");
|
||||||
|
row.put("overCostReimbursedName", row.getBoolean("overCostReimbursed", false) ? "是" : "否");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ExcelExportEntity> entities = new ArrayList<>();
|
||||||
|
entities.add(new ExcelExportEntity("序号", "index", 8));
|
||||||
|
entities.add(new ExcelExportEntity("年度", "year", 12));
|
||||||
|
entities.add(new ExcelExportEntity("工号", "jobNo", 18));
|
||||||
|
entities.add(new ExcelExportEntity("姓名", "userName", 16));
|
||||||
|
entities.add(new ExcelExportEntity("所在单位", "unitName", 28));
|
||||||
|
entities.add(new ExcelExportEntity("报名线路", "lineName", 30));
|
||||||
|
entities.add(new ExcelExportEntity("出行时段", "travelPeriod", 30));
|
||||||
|
entities.add(new ExcelExportEntity("线路类型", "lineType", 16));
|
||||||
|
entities.add(new ExcelExportEntity("成团信息", "groupInfo", 20));
|
||||||
|
entities.add(new ExcelExportEntity("领办工会", "undertakeUnionName", 24));
|
||||||
|
entities.add(new ExcelExportEntity("报名时间", "signupTime", 24));
|
||||||
|
entities.add(new ExcelExportEntity("携带家属人数", "familyCount", 16));
|
||||||
|
entities.add(new ExcelExportEntity("是否参加", "joinedName", 14));
|
||||||
|
entities.add(new ExcelExportEntity("是否申请超出费用由单位承担", "overCostReimbursedName", 28));
|
||||||
|
|
||||||
|
ExportParams exportParams = new ExportParams();
|
||||||
|
exportParams.setType(ExcelType.XSSF);
|
||||||
|
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, rows);
|
||||||
|
CommonDownloadUtil.download("疗休养台账.xlsx", workbook, response);
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("tour.ledger")
|
@SaCheckPermission("tour.ledger")
|
||||||
@Ok("void")
|
@Ok("void")
|
||||||
@@ -414,6 +473,18 @@ public class TourLedgerController {
|
|||||||
return unionRows;
|
return unionRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将成团编码和成团名称合并为列表一致的展示内容,任一字段为空时保留已有信息。
|
||||||
|
*/
|
||||||
|
private String formatGroupInfo(NutMap row) {
|
||||||
|
String groupCode = row.getString("groupCode", "");
|
||||||
|
String groupName = row.getString("groupName", "");
|
||||||
|
if (StrUtil.isBlank(groupCode)) {
|
||||||
|
return groupName;
|
||||||
|
}
|
||||||
|
return StrUtil.isBlank(groupName) ? groupCode : groupCode + " " + groupName;
|
||||||
|
}
|
||||||
|
|
||||||
private Workbook buildUnionSignupWorkbook(String unionName, List<NutMap> rows) {
|
private Workbook buildUnionSignupWorkbook(String unionName, List<NutMap> rows) {
|
||||||
HSSFWorkbook workbook = new HSSFWorkbook();
|
HSSFWorkbook workbook = new HSSFWorkbook();
|
||||||
Sheet sheet = workbook.createSheet("报名汇总表");
|
Sheet sheet = workbook.createSheet("报名汇总表");
|
||||||
|
|||||||
+86
-6
@@ -1,5 +1,9 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.tour.controller;
|
package com.budwk.app.zhgh.dayofficework.tour.controller;
|
||||||
|
|
||||||
|
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.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
@@ -7,6 +11,7 @@ import com.budwk.app.base.constant.RoleConstant;
|
|||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
import com.budwk.app.sys.models.Sys_dict;
|
import com.budwk.app.sys.models.Sys_dict;
|
||||||
import com.budwk.app.sys.services.SysDictService;
|
import com.budwk.app.sys.services.SysDictService;
|
||||||
import com.budwk.app.sys.views.View_user;
|
import com.budwk.app.sys.views.View_user;
|
||||||
@@ -26,7 +31,10 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
|||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -36,8 +44,6 @@ import java.util.stream.Collectors;
|
|||||||
@At("/platform/tour/route")
|
@At("/platform/tour/route")
|
||||||
public class TourLineController {
|
public class TourLineController {
|
||||||
|
|
||||||
private static final String SORT_CODE_PATTERN = "^\\d+$";
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private TourLineService tourLineService;
|
private TourLineService tourLineService;
|
||||||
|
|
||||||
@@ -52,7 +58,7 @@ public class TourLineController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("tour.route")
|
@SaCheckPermission("tour.route")
|
||||||
public Result pageData(PageForm pageForm, Integer year, String lineName, String lotId) {
|
public Result pageData(PageForm pageForm, Integer year, String lineName, String lotId, String lineType) {
|
||||||
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
||||||
pageForm.defaultSortAsc("lineCode");
|
pageForm.defaultSortAsc("lineCode");
|
||||||
}
|
}
|
||||||
@@ -62,6 +68,8 @@ public class TourLineController {
|
|||||||
cnd.andEX("l.`year`", "=", year);
|
cnd.andEX("l.`year`", "=", year);
|
||||||
cnd.and(Cnd.likeEX("l.lineName", lineName));
|
cnd.and(Cnd.likeEX("l.lineName", lineName));
|
||||||
cnd.andEX("l.lotId", "=", lotId);
|
cnd.andEX("l.lotId", "=", lotId);
|
||||||
|
// 线路类型查询使用线路表保存的字典名称,和新增/编辑线路时的取值保持一致。
|
||||||
|
cnd.andEX("l.lineType", "=", lineType);
|
||||||
boolean sysAdmin = AuthUtil.hasRole(RoleConstant.SYSADMIN.name());
|
boolean sysAdmin = AuthUtil.hasRole(RoleConstant.SYSADMIN.name());
|
||||||
boolean branchUnionScope = !sysAdmin && AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name());
|
boolean branchUnionScope = !sysAdmin && AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name());
|
||||||
applyDataScope(cnd, sysAdmin, branchUnionScope);
|
applyDataScope(cnd, sysAdmin, branchUnionScope);
|
||||||
@@ -107,6 +115,78 @@ public class TourLineController {
|
|||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出线路关联事项数据,导出范围复用线路管理列表的筛选条件和数据权限。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@SaCheckPermission("tour.route")
|
||||||
|
public void exportData(Integer year, String lineName, String lotId, String lineType, HttpServletResponse response) {
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
cnd.and("l.delFlag", "=", false);
|
||||||
|
cnd.andEX("l.`year`", "=", year);
|
||||||
|
cnd.and(Cnd.likeEX("l.lineName", lineName));
|
||||||
|
cnd.andEX("l.lotId", "=", lotId);
|
||||||
|
// 导出范围复用列表筛选条件,线路类型也要同步过滤。
|
||||||
|
cnd.andEX("l.lineType", "=", lineType);
|
||||||
|
boolean sysAdmin = AuthUtil.hasRole(RoleConstant.SYSADMIN.name());
|
||||||
|
boolean branchUnionScope = !sysAdmin && AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name());
|
||||||
|
applyDataScope(cnd, sysAdmin, branchUnionScope);
|
||||||
|
String creatorJoin = branchUnionScope ? """
|
||||||
|
LEFT JOIN sys_unit lineUnit ON lineUnit.id = l.unitId
|
||||||
|
LEFT JOIN sys_user creatorUser ON creatorUser.id = COALESCE(NULLIF(l.creatorUserId, ''), l.createdBy)
|
||||||
|
LEFT JOIN sys_unit creatorUnit ON creatorUnit.id = creatorUser.unitId
|
||||||
|
""" : "";
|
||||||
|
|
||||||
|
// 线路导出需要带出事项信息,因此以线路为主表左关联事项,未关联事项的线路仍保留导出。
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
l.`year`,
|
||||||
|
l.lineName,
|
||||||
|
lot.lotName,
|
||||||
|
l.lineType,
|
||||||
|
m.matterName,
|
||||||
|
CASE
|
||||||
|
WHEN IFNULL(m.signupStartTime, '') <> '' AND IFNULL(m.signupEndTime, '') <> ''
|
||||||
|
THEN CONCAT(m.signupStartTime, ' 至 ', m.signupEndTime)
|
||||||
|
ELSE ''
|
||||||
|
END AS signupTime,
|
||||||
|
CASE
|
||||||
|
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
||||||
|
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
||||||
|
ELSE ''
|
||||||
|
END AS travelTime,
|
||||||
|
m.contactName,
|
||||||
|
m.contactPhone
|
||||||
|
FROM tour_line l
|
||||||
|
LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId
|
||||||
|
LEFT JOIN tour_matter m ON l.id = m.lineId AND m.delFlag = 0
|
||||||
|
$creatorJoin
|
||||||
|
$condition
|
||||||
|
ORDER BY CAST(l.lineCode AS UNSIGNED) ASC, l.lineCode ASC, m.matterName ASC, l.lineName ASC
|
||||||
|
""");
|
||||||
|
sql.setVar("creatorJoin", creatorJoin);
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
tourLineService.dao().execute(sql);
|
||||||
|
|
||||||
|
List<ExcelExportEntity> entities = new ArrayList<>();
|
||||||
|
entities.add(new ExcelExportEntity("创建年度", "year", 14));
|
||||||
|
entities.add(new ExcelExportEntity("线路名称", "lineName", 30));
|
||||||
|
entities.add(new ExcelExportEntity("时间标段", "lotName", 18));
|
||||||
|
entities.add(new ExcelExportEntity("路线类型", "lineType", 18));
|
||||||
|
entities.add(new ExcelExportEntity("事项名称", "matterName", 30));
|
||||||
|
entities.add(new ExcelExportEntity("报名时间", "signupTime", 34));
|
||||||
|
entities.add(new ExcelExportEntity("出行时间", "travelTime", 24));
|
||||||
|
entities.add(new ExcelExportEntity("联系人", "contactName", 16));
|
||||||
|
entities.add(new ExcelExportEntity("联系方式", "contactPhone", 18));
|
||||||
|
|
||||||
|
ExportParams exportParams = new ExportParams();
|
||||||
|
exportParams.setType(ExcelType.XSSF);
|
||||||
|
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, sql.getList(NutMap.class));
|
||||||
|
CommonDownloadUtil.download("线路事项数据.xlsx", workbook, response);
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("tour.route")
|
@SaCheckPermission("tour.route")
|
||||||
public Result detail(String id) {
|
public Result detail(String id) {
|
||||||
@@ -253,11 +333,11 @@ public class TourLineController {
|
|||||||
if (StrUtil.isBlank(line.getLineName())) {
|
if (StrUtil.isBlank(line.getLineName())) {
|
||||||
return Result.error("线路名称不能为空");
|
return Result.error("线路名称不能为空");
|
||||||
}
|
}
|
||||||
if (StrUtil.isBlank(line.getLineCode())) {
|
if (line.getLineCode() == null) {
|
||||||
return Result.error("排序编号不能为空");
|
return Result.error("排序编号不能为空");
|
||||||
}
|
}
|
||||||
if (!line.getLineCode().matches(SORT_CODE_PATTERN)) {
|
if (line.getLineCode() < 0) {
|
||||||
return Result.error("排序编号必须为整数");
|
return Result.error("排序编号不能小于0");
|
||||||
}
|
}
|
||||||
if (StrUtil.isBlank(line.getLineType())) {
|
if (StrUtil.isBlank(line.getLineType())) {
|
||||||
return Result.error("线路类型不能为空");
|
return Result.error("线路类型不能为空");
|
||||||
|
|||||||
+159
-22
@@ -1,12 +1,17 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.tour.controller;
|
package com.budwk.app.zhgh.dayofficework.tour.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
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.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
import com.budwk.app.sys.models.Sys_dict;
|
import com.budwk.app.sys.models.Sys_dict;
|
||||||
import com.budwk.app.sys.models.Sys_union;
|
import com.budwk.app.sys.models.Sys_union;
|
||||||
import com.budwk.app.sys.services.SysDictService;
|
import com.budwk.app.sys.services.SysDictService;
|
||||||
@@ -27,7 +32,10 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
|||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -53,18 +61,13 @@ public class TourMatterController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("tour.matter")
|
@SaCheckPermission("tour.matter")
|
||||||
public Result pageData(PageForm pageForm, Integer year, String matterName, String unionId, String organizationType) {
|
public Result pageData(PageForm pageForm, Integer year, String matterName, String unionId, String organizationType, String lineType) {
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = buildMatterPageCnd(year, matterName, unionId, organizationType, lineType);
|
||||||
cnd.and("m.delFlag", "=", false);
|
|
||||||
cnd.andEX("m.`year`", "=", year);
|
|
||||||
cnd.and(Cnd.likeEX("m.matterName", matterName));
|
|
||||||
cnd.andEX("m.unionId", "=", unionId);
|
|
||||||
cnd.andEX("m.organizationType", "=", organizationType);
|
|
||||||
applyMatterDataScope(cnd);
|
|
||||||
|
|
||||||
Sql countSql = Sqls.create("""
|
Sql countSql = Sqls.create("""
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
FROM tour_matter m
|
FROM tour_matter m
|
||||||
|
LEFT JOIN tour_line l ON l.id = m.lineId
|
||||||
$condition
|
$condition
|
||||||
""");
|
""");
|
||||||
countSql.setCondition(cnd);
|
countSql.setCondition(cnd);
|
||||||
@@ -91,7 +94,7 @@ public class TourMatterController {
|
|||||||
LEFT JOIN sys_dict d ON d.`code` = m.organizationType
|
LEFT JOIN sys_dict d ON d.`code` = m.organizationType
|
||||||
AND d.parentId = (SELECT id FROM sys_dict WHERE `code` = 'organizationType' LIMIT 1)
|
AND d.parentId = (SELECT id FROM sys_dict WHERE `code` = 'organizationType' LIMIT 1)
|
||||||
$condition
|
$condition
|
||||||
ORDER BY $orderColumn $orderBy, m.`year` DESC, m.createdAt DESC
|
ORDER BY $orderColumn $orderBy, CAST(l.lineCode AS UNSIGNED) ASC, m.`year` DESC, m.createdAt DESC
|
||||||
""");
|
""");
|
||||||
listSql.setCondition(cnd);
|
listSql.setCondition(cnd);
|
||||||
listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName()));
|
listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName()));
|
||||||
@@ -104,6 +107,109 @@ public class TourMatterController {
|
|||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出疗休养事项列表,导出范围复用页面查询条件和事项数据权限。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@SaCheckPermission("tour.matter")
|
||||||
|
public void exportData(Integer year, String matterName, String unionId, String organizationType, String lineType,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
Cnd cnd = buildMatterPageCnd(year, matterName, unionId, organizationType, lineType);
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
m.`year`,
|
||||||
|
m.matterName,
|
||||||
|
l.lineName,
|
||||||
|
CASE
|
||||||
|
WHEN IFNULL(m.signupStartTime, '') <> '' AND IFNULL(m.signupEndTime, '') <> ''
|
||||||
|
THEN CONCAT(m.signupStartTime, ' 至 ', m.signupEndTime)
|
||||||
|
ELSE ''
|
||||||
|
END AS signupTime,
|
||||||
|
CASE
|
||||||
|
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
||||||
|
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
||||||
|
ELSE ''
|
||||||
|
END AS travelTime,
|
||||||
|
d.name AS organizationTypeName,
|
||||||
|
m.contactName,
|
||||||
|
m.contactPhone,
|
||||||
|
CASE WHEN m.enabled = 1 THEN '启用' ELSE '禁用' END AS enabledName
|
||||||
|
FROM tour_matter m
|
||||||
|
LEFT JOIN tour_line l ON l.id = m.lineId
|
||||||
|
LEFT JOIN sys_union u ON u.id = m.unionId
|
||||||
|
LEFT JOIN sys_dict d ON d.`code` = m.organizationType
|
||||||
|
AND d.parentId = (SELECT id FROM sys_dict WHERE `code` = 'organizationType' LIMIT 1)
|
||||||
|
$condition
|
||||||
|
ORDER BY CAST(l.lineCode AS UNSIGNED) ASC, m.`year` DESC, m.createdAt DESC
|
||||||
|
""");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
tourMatterService.dao().execute(sql);
|
||||||
|
|
||||||
|
List<NutMap> rows = sql.getList(NutMap.class);
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
rows.get(i).put("index", i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ExcelExportEntity> entities = new ArrayList<>();
|
||||||
|
entities.add(new ExcelExportEntity("序号", "index", 8));
|
||||||
|
entities.add(new ExcelExportEntity("年度", "year", 12));
|
||||||
|
entities.add(new ExcelExportEntity("事项名称", "matterName", 30));
|
||||||
|
entities.add(new ExcelExportEntity("线路名称", "lineName", 30));
|
||||||
|
entities.add(new ExcelExportEntity("报名时间", "signupTime", 34));
|
||||||
|
entities.add(new ExcelExportEntity("出行时间", "travelTime", 24));
|
||||||
|
entities.add(new ExcelExportEntity("组织形式", "organizationTypeName", 16));
|
||||||
|
entities.add(new ExcelExportEntity("联系人", "contactName", 16));
|
||||||
|
entities.add(new ExcelExportEntity("联系方式", "contactPhone", 18));
|
||||||
|
entities.add(new ExcelExportEntity("事项状态", "enabledName", 12));
|
||||||
|
|
||||||
|
ExportParams exportParams = new ExportParams();
|
||||||
|
exportParams.setType(ExcelType.XSSF);
|
||||||
|
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, rows);
|
||||||
|
CommonDownloadUtil.download("疗休养事项数据.xlsx", workbook, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按当前事项列表查询条件统一赋值报名时间,更新前端筛选范围内的全部事项。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@SaCheckPermission("tour.matter")
|
||||||
|
@SLog(type = "tour", tag = "疗休养事项", msg = "统一赋值报名时间")
|
||||||
|
public Result assignSignupTime(Integer year, String matterName, String unionId, String organizationType, String lineType,
|
||||||
|
String signupStartTime, String signupEndTime) {
|
||||||
|
if (StrUtil.isBlank(signupStartTime) || StrUtil.isBlank(signupEndTime)) {
|
||||||
|
return Result.error("报名开始时间和报名结束时间不能为空");
|
||||||
|
}
|
||||||
|
if (signupStartTime.compareTo(signupEndTime) >= 0) {
|
||||||
|
return Result.error("报名开始时间必须小于报名结束时间");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量赋值必须严格复用事项列表查询条件,确保“查询哪些数据就赋值哪些事项”。
|
||||||
|
Cnd cnd = buildMatterPageCnd(year, matterName, unionId, organizationType, lineType);
|
||||||
|
Sql idSql = Sqls.create("""
|
||||||
|
SELECT m.id
|
||||||
|
FROM tour_matter m
|
||||||
|
LEFT JOIN tour_line l ON l.id = m.lineId
|
||||||
|
$condition
|
||||||
|
""");
|
||||||
|
idSql.setCondition(cnd);
|
||||||
|
idSql.setCallback(Sqls.callback.maps());
|
||||||
|
tourMatterService.dao().execute(idSql);
|
||||||
|
List<String> ids = idSql.getList(NutMap.class).stream()
|
||||||
|
.map(item -> item.getString("id"))
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return Result.success("当前查询条件下暂无可赋值事项");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只更新报名时间字段,不影响事项的线路、联系人、成团人数等其他配置。
|
||||||
|
int updateCount = tourMatterService.assignSignupTime(ids, signupStartTime, signupEndTime);
|
||||||
|
return Result.success("赋值成功,共更新" + updateCount + "条事项");
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("tour.matter")
|
@SaCheckPermission("tour.matter")
|
||||||
public Result detail(String id) {
|
public Result detail(String id) {
|
||||||
@@ -142,17 +248,20 @@ public class TourMatterController {
|
|||||||
cnd.and("l.enabled", "=", true);
|
cnd.and("l.enabled", "=", true);
|
||||||
cnd.and("l.openFlag", "=", true);
|
cnd.and("l.openFlag", "=", true);
|
||||||
|
|
||||||
|
// 事项选择线路下拉需要展示线路类型和旅行社,因此这里补充旅行社名称返回给前端拼接显示。
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
l.*,
|
l.*,
|
||||||
|
a.agencyName AS travelAgencyName,
|
||||||
lot.lotName AS lotName,
|
lot.lotName AS lotName,
|
||||||
lot.lotValue AS lotValue,
|
lot.lotValue AS lotValue,
|
||||||
lot.lotValue AS lotDays,
|
lot.lotValue AS lotDays,
|
||||||
lot.activityCost AS activityCost
|
lot.activityCost AS activityCost
|
||||||
FROM tour_line l
|
FROM tour_line l
|
||||||
|
LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId
|
||||||
LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId
|
LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId
|
||||||
$condition
|
$condition
|
||||||
ORDER BY l.lineCode ASC, l.lineName ASC
|
ORDER BY CAST(l.lineCode AS UNSIGNED) ASC, l.lineName ASC
|
||||||
""");
|
""");
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
sql.setCallback(Sqls.callback.maps());
|
sql.setCallback(Sqls.callback.maps());
|
||||||
@@ -199,6 +308,17 @@ public class TourMatterController {
|
|||||||
return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList()));
|
return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("tour.matter")
|
||||||
|
public Result lineTypeOptions() {
|
||||||
|
// 事项列表线路类型查询复用线路管理的 lineType 字典,保证筛选值与线路保存值一致。
|
||||||
|
List<Sys_dict> list = sysDictService.getSubListByCode("lineType");
|
||||||
|
if (list == null) {
|
||||||
|
return Result.success(Collections.emptyList());
|
||||||
|
}
|
||||||
|
return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@SaCheckPermission("tour.matter")
|
@SaCheckPermission("tour.matter")
|
||||||
@@ -328,6 +448,7 @@ public class TourMatterController {
|
|||||||
oldMatter.setContactPhone(matter.getContactPhone());
|
oldMatter.setContactPhone(matter.getContactPhone());
|
||||||
oldMatter.setMinGroupPeople(matter.getMinGroupPeople());
|
oldMatter.setMinGroupPeople(matter.getMinGroupPeople());
|
||||||
oldMatter.setMaxGroupPeople(matter.getMaxGroupPeople());
|
oldMatter.setMaxGroupPeople(matter.getMaxGroupPeople());
|
||||||
|
oldMatter.setMaxSignupPeople(matter.getMaxSignupPeople());
|
||||||
oldMatter.setEstimatedCost(matter.getEstimatedCost());
|
oldMatter.setEstimatedCost(matter.getEstimatedCost());
|
||||||
tourMatterService.update(oldMatter);
|
tourMatterService.update(oldMatter);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
@@ -348,16 +469,14 @@ public class TourMatterController {
|
|||||||
if (setting == null) {
|
if (setting == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (setting.getMinGroupPeople() != null) {
|
// 配置只作为弹窗缺省值,不能覆盖用户在选择线路页面已经提交的成团人数和报名时间。
|
||||||
|
if (matter.getMinGroupPeople() == null && setting.getMinGroupPeople() != null) {
|
||||||
matter.setMinGroupPeople(setting.getMinGroupPeople());
|
matter.setMinGroupPeople(setting.getMinGroupPeople());
|
||||||
}
|
}
|
||||||
if (setting.getMaxGroupPeople() != null) {
|
if (StrUtil.isBlank(matter.getSignupStartTime()) && StrUtil.isNotBlank(setting.getSignupStartTime())) {
|
||||||
matter.setMaxGroupPeople(setting.getMaxGroupPeople());
|
|
||||||
}
|
|
||||||
if (StrUtil.isNotBlank(setting.getSignupStartTime())) {
|
|
||||||
matter.setSignupStartTime(setting.getSignupStartTime());
|
matter.setSignupStartTime(setting.getSignupStartTime());
|
||||||
}
|
}
|
||||||
if (StrUtil.isNotBlank(setting.getSignupEndTime())) {
|
if (StrUtil.isBlank(matter.getSignupEndTime()) && StrUtil.isNotBlank(setting.getSignupEndTime())) {
|
||||||
matter.setSignupEndTime(setting.getSignupEndTime());
|
matter.setSignupEndTime(setting.getSignupEndTime());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,11 +548,9 @@ public class TourMatterController {
|
|||||||
if (matter.getMinGroupPeople() == null || matter.getMinGroupPeople() <= 0) {
|
if (matter.getMinGroupPeople() == null || matter.getMinGroupPeople() <= 0) {
|
||||||
return Result.error("最少成团人数必须大于0");
|
return Result.error("最少成团人数必须大于0");
|
||||||
}
|
}
|
||||||
if (matter.getMaxGroupPeople() == null || matter.getMaxGroupPeople() <= 0) {
|
// 最多报名人数只控制报名提交容量;为空或0表示不限制,不能影响成团分配人数。
|
||||||
return Result.error("最多成团人数必须大于0");
|
if (matter.getMaxSignupPeople() != null && matter.getMaxSignupPeople() < 0) {
|
||||||
}
|
return Result.error("最多报名人数不能小于0");
|
||||||
if (matter.getMinGroupPeople() > matter.getMaxGroupPeople()) {
|
|
||||||
return Result.error("最少成团人数不能大于最多成团人数");
|
|
||||||
}
|
}
|
||||||
if (matter.getEstimatedCost() == null || matter.getEstimatedCost().signum() < 0) {
|
if (matter.getEstimatedCost() == null || matter.getEstimatedCost().signum() < 0) {
|
||||||
return Result.error("预计费用不能小于0");
|
return Result.error("预计费用不能小于0");
|
||||||
@@ -462,6 +579,22 @@ public class TourMatterController {
|
|||||||
.and(TourLedger::getMatterId, "=", matterId));
|
.and(TourLedger::getMatterId, "=", matterId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建事项列表查询条件,批量赋值报名时间时复用该条件保证更新范围与列表查询一致。
|
||||||
|
*/
|
||||||
|
private Cnd buildMatterPageCnd(Integer year, String matterName, String unionId, String organizationType, String lineType) {
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
cnd.and("m.delFlag", "=", false);
|
||||||
|
cnd.andEX("m.`year`", "=", year);
|
||||||
|
cnd.and(Cnd.likeEX("m.matterName", matterName));
|
||||||
|
cnd.andEX("m.unionId", "=", unionId);
|
||||||
|
cnd.andEX("m.organizationType", "=", organizationType);
|
||||||
|
// 事项列表和批量赋值报名时间共用该条件,按线路类型筛选时必须关联线路表字段。
|
||||||
|
cnd.andEX("l.lineType", "=", lineType);
|
||||||
|
applyMatterDataScope(cnd);
|
||||||
|
return cnd;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事项数据范围:SYSADMIN 查看全部;分工会主席查看本工会事项;其它角色仅查看自己创建的事项。
|
* 事项数据范围:SYSADMIN 查看全部;分工会主席查看本工会事项;其它角色仅查看自己创建的事项。
|
||||||
*/
|
*/
|
||||||
@@ -512,6 +645,10 @@ public class TourMatterController {
|
|||||||
if ("lineName".equals(orderName)) {
|
if ("lineName".equals(orderName)) {
|
||||||
return "l.lineName";
|
return "l.lineName";
|
||||||
}
|
}
|
||||||
|
if ("lineCode".equals(orderName)) {
|
||||||
|
// 线路序号按数字排序,避免 10 排在 2 前面。
|
||||||
|
return "CAST(l.lineCode AS UNSIGNED)";
|
||||||
|
}
|
||||||
if ("settingName".equals(orderName)) {
|
if ("settingName".equals(orderName)) {
|
||||||
return "s.configName";
|
return "s.configName";
|
||||||
}
|
}
|
||||||
@@ -521,6 +658,6 @@ public class TourMatterController {
|
|||||||
if ("enabled".equals(orderName)) {
|
if ("enabled".equals(orderName)) {
|
||||||
return "m.enabled";
|
return "m.enabled";
|
||||||
}
|
}
|
||||||
return "m.`year`";
|
return "CAST(l.lineCode AS UNSIGNED)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-48
@@ -31,6 +31,7 @@ import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeSer
|
|||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
@@ -83,6 +84,9 @@ public class TourMySignupController {
|
|||||||
@Inject
|
@Inject
|
||||||
private TourMatterService tourMatterService;
|
private TourMatterService tourMatterService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private TourParticipantRosterService tourParticipantRosterService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private SysDictService sysDictService;
|
private SysDictService sysDictService;
|
||||||
|
|
||||||
@@ -145,6 +149,8 @@ public class TourMySignupController {
|
|||||||
IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName,
|
IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName,
|
||||||
IFNULL(f.familyCount, 0) AS familyCount,
|
IFNULL(f.familyCount, 0) AS familyCount,
|
||||||
COALESCE(m.travelEndTime, tp.travelEndTime) AS travelEndTime,
|
COALESCE(m.travelEndTime, tp.travelEndTime) AS travelEndTime,
|
||||||
|
COALESCE(NULLIF(s.signupStartTime, ''), m.signupStartTime) AS signupStartTime,
|
||||||
|
COALESCE(NULLIF(s.signupEndTime, ''), m.signupEndTime) AS signupEndTime,
|
||||||
CASE
|
CASE
|
||||||
WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN COALESCE(u.name, '校工会')
|
WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN COALESCE(u.name, '校工会')
|
||||||
ELSE ''
|
ELSE ''
|
||||||
@@ -166,6 +172,7 @@ public class TourMySignupController {
|
|||||||
GROUP BY ledgerId
|
GROUP BY ledgerId
|
||||||
) f ON f.ledgerId = t.id
|
) f ON f.ledgerId = t.id
|
||||||
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
||||||
|
LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0
|
||||||
LEFT JOIN sys_union u ON u.id = m.unionId
|
LEFT JOIN sys_union u ON u.id = m.unionId
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -290,6 +297,9 @@ public class TourMySignupController {
|
|||||||
m.unionId AS matterUnionId,
|
m.unionId AS matterUnionId,
|
||||||
m.travelStartTime,
|
m.travelStartTime,
|
||||||
m.travelEndTime,
|
m.travelEndTime,
|
||||||
|
COALESCE(NULLIF(s.signupStartTime, ''), m.signupStartTime) AS signupStartTime,
|
||||||
|
COALESCE(NULLIF(s.signupEndTime, ''), m.signupEndTime) AS signupEndTime,
|
||||||
|
m.maxSignupPeople,
|
||||||
CASE
|
CASE
|
||||||
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
||||||
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
||||||
@@ -323,6 +333,10 @@ public class TourMySignupController {
|
|||||||
if (isTravelEnded(matter.getString("travelEndTime"))) {
|
if (isTravelEnded(matter.getString("travelEndTime"))) {
|
||||||
return Result.error("线路出行已结束,不能修改");
|
return Result.error("线路出行已结束,不能修改");
|
||||||
}
|
}
|
||||||
|
Result signupEndResult = checkSignupEndTime(ledger);
|
||||||
|
if (signupEndResult != null) {
|
||||||
|
return signupEndResult;
|
||||||
|
}
|
||||||
Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false)
|
Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false)
|
||||||
.and(TourLedgerFamily::getLedgerId, "=", ledger.getId());
|
.and(TourLedgerFamily::getLedgerId, "=", ledger.getId());
|
||||||
familyCnd.asc(TourLedgerFamily::getCreatedAt);
|
familyCnd.asc(TourLedgerFamily::getCreatedAt);
|
||||||
@@ -440,14 +454,10 @@ public class TourMySignupController {
|
|||||||
return familyResult;
|
return familyResult;
|
||||||
}
|
}
|
||||||
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
|
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
|
||||||
Result ruleResult = checkSignupRule(ledger, matter, oldLedger);
|
Result ruleResult = checkSignupRule(ledger, matter, oldLedger, calculateCurrentSignupPeople(familyList));
|
||||||
if (ruleResult != null) {
|
if (ruleResult != null) {
|
||||||
return ruleResult;
|
return ruleResult;
|
||||||
}
|
}
|
||||||
Result maxPeopleResult = checkMaxGroupPeople(matter, oldLedger, familyList);
|
|
||||||
if (maxPeopleResult != null) {
|
|
||||||
return maxPeopleResult;
|
|
||||||
}
|
|
||||||
TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId());
|
TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId());
|
||||||
boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine());
|
boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine());
|
||||||
TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId());
|
TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId());
|
||||||
@@ -501,6 +511,8 @@ public class TourMySignupController {
|
|||||||
return workflowResult;
|
return workflowResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Signup modification keeps the staff member joined and refreshes the linked ledger id.
|
||||||
|
tourParticipantRosterService.markJoinedByLedger(ledger);
|
||||||
return Result.success().addMsg(approvalRequired ? "修改已提交,等待审核" : "修改成功");
|
return Result.success().addMsg(approvalRequired ? "修改已提交,等待审核" : "修改成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,6 +531,12 @@ public class TourMySignupController {
|
|||||||
if (ledger == null) {
|
if (ledger == null) {
|
||||||
return Result.error("报名记录不存在");
|
return Result.error("报名记录不存在");
|
||||||
}
|
}
|
||||||
|
Result signupEndResult = checkSignupEndTime(ledger);
|
||||||
|
if (signupEndResult != null) {
|
||||||
|
return signupEndResult;
|
||||||
|
}
|
||||||
|
// Reset the roster before deleting the ledger record, because the ledger still carries setting linkage here.
|
||||||
|
tourParticipantRosterService.markNotJoinedByLedger(ledger);
|
||||||
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
|
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
|
||||||
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
|
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
|
||||||
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
|
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
|
||||||
@@ -534,6 +552,12 @@ public class TourMySignupController {
|
|||||||
if (ledger == null) {
|
if (ledger == null) {
|
||||||
return Result.error("报名记录不存在");
|
return Result.error("报名记录不存在");
|
||||||
}
|
}
|
||||||
|
Result signupEndResult = checkSignupEndTime(ledger);
|
||||||
|
if (signupEndResult != null) {
|
||||||
|
return signupEndResult;
|
||||||
|
}
|
||||||
|
// Reset the roster before deleting the ledger record, because the ledger still carries setting linkage here.
|
||||||
|
tourParticipantRosterService.markNotJoinedByLedger(ledger);
|
||||||
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
|
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
|
||||||
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
|
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
|
||||||
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
|
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
|
||||||
@@ -604,7 +628,30 @@ public class TourMySignupController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result checkSignupRule(TourLedger ledger, TourMatter matter, TourLedger oldLedger) {
|
/**
|
||||||
|
* 修改和取消报名都必须受有效报名结束时间控制,避免前端隐藏按钮后仍可直接调用接口。
|
||||||
|
*/
|
||||||
|
private Result checkSignupEndTime(TourLedger ledger) {
|
||||||
|
if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
|
||||||
|
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId());
|
||||||
|
String signupEndTime = defaultIfBlank(setting == null ? "" : setting.getSignupEndTime(), matter.getSignupEndTime());
|
||||||
|
if (StrUtil.isBlank(signupEndTime)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
if (now.compareTo(normalizeDateTime(signupEndTime, true)) > 0) {
|
||||||
|
return Result.error("报名已结束,不能修改或取消报名");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkSignupRule(TourLedger ledger, TourMatter matter, TourLedger oldLedger, int currentPeopleCount) {
|
||||||
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) {
|
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) {
|
||||||
return Result.error("报名出行时段不存在或已停用");
|
return Result.error("报名出行时段不存在或已停用");
|
||||||
}
|
}
|
||||||
@@ -622,6 +669,10 @@ public class TourMySignupController {
|
|||||||
if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) {
|
if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) {
|
||||||
return Result.error("报名事项配置不存在或已停用");
|
return Result.error("报名事项配置不存在或已停用");
|
||||||
}
|
}
|
||||||
|
Result maxSignupResult = checkMaxSignupPeople(matter, oldLedger, currentPeopleCount);
|
||||||
|
if (maxSignupResult != null) {
|
||||||
|
return maxSignupResult;
|
||||||
|
}
|
||||||
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
|
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
|
||||||
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
|
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
|
||||||
return Result.error("您不在本次疗休养报名范围内");
|
return Result.error("您不在本次疗休养报名范围内");
|
||||||
@@ -661,6 +712,8 @@ public class TourMySignupController {
|
|||||||
if (tourLedgerService.count(outProvinceCnd) > 0) {
|
if (tourLedgerService.count(outProvinceCnd) > 0) {
|
||||||
return Result.error("您已参加过省外线路,不能报名省外线路,请选择省内线路");
|
return Result.error("您已参加过省外线路,不能报名省外线路,请选择省内线路");
|
||||||
}
|
}
|
||||||
|
// Out-province quota is shared by year; lock only before the quota count calculation.
|
||||||
|
tourLedgerService.lockTourSignupQuotaYear(matter.getYear());
|
||||||
OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger);
|
OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger);
|
||||||
if (!quota.canApply()) {
|
if (!quota.canApply()) {
|
||||||
return Result.error(buildOutProvinceQuotaNotice(quota));
|
return Result.error(buildOutProvinceQuotaNotice(quota));
|
||||||
@@ -692,6 +745,61 @@ public class TourMySignupController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the matter maximum signup capacity at final submit time.
|
||||||
|
*/
|
||||||
|
private Result checkMaxSignupPeople(TourMatter matter, TourLedger oldLedger, int currentPeopleCount) {
|
||||||
|
int maxSignupPeople = matter == null || matter.getMaxSignupPeople() == null ? 0 : matter.getMaxSignupPeople();
|
||||||
|
if (maxSignupPeople <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Maximum signup capacity is owned by the current matter, so only the same matter is serialized.
|
||||||
|
tourLedgerService.lockTourSignupMatter(matter.getId());
|
||||||
|
String excludeLedgerId = oldLedger == null ? null : oldLedger.getId();
|
||||||
|
int signedPeople = countMatterSignupPeople(matter.getId(), excludeLedgerId);
|
||||||
|
if (signedPeople + currentPeopleCount > maxSignupPeople) {
|
||||||
|
return Result.error("当前线路最多报名人数为" + maxSignupPeople + "人,当前已报名" + signedPeople
|
||||||
|
+ "人,本次报名" + currentPeopleCount + "人,不能超过最多报名人数");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts current submit people as one staff member plus all submitted family members.
|
||||||
|
*/
|
||||||
|
private int calculateCurrentSignupPeople(List<TourLedgerFamily> familyList) {
|
||||||
|
return 1 + (familyList == null ? 0 : familyList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts valid signed people for a matter, including staff and their family members.
|
||||||
|
*/
|
||||||
|
private int countMatterSignupPeople(String matterId, String excludeLedgerId) {
|
||||||
|
if (StrUtil.isBlank(matterId)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId";
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS peopleCount
|
||||||
|
FROM tour_ledger t
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
|
FROM tour_ledger_family
|
||||||
|
WHERE delFlag = 0
|
||||||
|
GROUP BY ledgerId
|
||||||
|
) f ON f.ledgerId = t.id
|
||||||
|
WHERE t.delFlag = 0
|
||||||
|
AND t.matterId = @matterId
|
||||||
|
""" + excludeSql);
|
||||||
|
sql.setParam("matterId", matterId);
|
||||||
|
if (StrUtil.isNotBlank(excludeLedgerId)) {
|
||||||
|
sql.setParam("excludeLedgerId", excludeLedgerId);
|
||||||
|
}
|
||||||
|
sql.setCallback(Sqls.callback.integer());
|
||||||
|
tourLedgerService.dao().execute(sql);
|
||||||
|
return sql.getInt();
|
||||||
|
}
|
||||||
|
|
||||||
private TourLedger fetchLatestLedger(Cnd cnd) {
|
private TourLedger fetchLatestLedger(Cnd cnd) {
|
||||||
cnd.desc(TourLedger::getSignupTime);
|
cnd.desc(TourLedger::getSignupTime);
|
||||||
cnd.desc(TourLedger::getCreatedAt);
|
cnd.desc(TourLedger::getCreatedAt);
|
||||||
@@ -985,46 +1093,6 @@ public class TourMySignupController {
|
|||||||
&& idCard.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dX]$");
|
&& idCard.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dX]$");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result checkMaxGroupPeople(TourMatter matter, TourLedger oldLedger, List<TourLedgerFamily> familyList) {
|
|
||||||
if (matter == null || matter.getMaxGroupPeople() == null || matter.getMaxGroupPeople() <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int currentPeople = 1 + (Lang.isEmpty(familyList) ? 0 : familyList.size());
|
|
||||||
int signedPeople = countSignupPeople(matter.getId(), oldLedger == null ? null : oldLedger.getId());
|
|
||||||
int totalPeople = signedPeople + currentPeople;
|
|
||||||
if (totalPeople > matter.getMaxGroupPeople()) {
|
|
||||||
return Result.error("当前报名人数 " + signedPeople + " 人,本次报名 " + currentPeople
|
|
||||||
+ " 人,最多成团人数 " + matter.getMaxGroupPeople() + " 人,报名后将超过人数上限,不能提交");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int countSignupPeople(String matterId, String excludeLedgerId) {
|
|
||||||
if (StrUtil.isBlank(matterId)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId";
|
|
||||||
Sql sql = Sqls.create("""
|
|
||||||
SELECT IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS signupPeople
|
|
||||||
FROM tour_ledger t
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT ledgerId, COUNT(1) AS familyCount
|
|
||||||
FROM tour_ledger_family
|
|
||||||
WHERE delFlag = 0
|
|
||||||
GROUP BY ledgerId
|
|
||||||
) f ON f.ledgerId = t.id
|
|
||||||
WHERE t.delFlag = 0
|
|
||||||
AND t.matterId = @matterId
|
|
||||||
""" + excludeSql);
|
|
||||||
sql.setParam("matterId", matterId);
|
|
||||||
if (StrUtil.isNotBlank(excludeLedgerId)) {
|
|
||||||
sql.setParam("excludeLedgerId", excludeLedgerId);
|
|
||||||
}
|
|
||||||
sql.setCallback(Sqls.callback.integer());
|
|
||||||
tourLedgerService.dao().execute(sql);
|
|
||||||
return sql.getInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildSelectedLineMessage(TourLedger ledger) {
|
private String buildSelectedLineMessage(TourLedger ledger) {
|
||||||
String selectedLineName = getLedgerLineName(ledger);
|
String selectedLineName = getLedgerLineName(ledger);
|
||||||
return "您已经选择【" + selectedLineName + "】,取消后再重新选择。";
|
return "您已经选择【" + selectedLineName + "】,取消后再重新选择。";
|
||||||
@@ -1650,14 +1718,15 @@ public class TourMySignupController {
|
|||||||
|
|
||||||
private PDType0Font loadChineseFont(PDDocument document, boolean bold) throws IOException {
|
private PDType0Font loadChineseFont(PDDocument document, boolean bold) throws IOException {
|
||||||
String[] paths = bold
|
String[] paths = bold
|
||||||
? new String[]{"C:/Windows/Fonts/simhei.ttf", "C:/Windows/Fonts/msyhbd.ttf", "C:/Windows/Fonts/simsun.ttf"}
|
? new String[]{"/usr/share/fonts/chinese/simhei.ttf", "/usr/share/fonts/chinese/msyhbd.ttc", "/usr/share/fonts/chinese/simsun.ttc"}
|
||||||
: new String[]{"C:/Windows/Fonts/simfang.ttf", "C:/Windows/Fonts/simsun.ttf", "C:/Windows/Fonts/msyh.ttf"};
|
: new String[]{"/usr/share/fonts/chinese/simfang.ttf", "/usr/share/fonts/chinese/simsun.ttc", "/usr/share/fonts/chinese/msyh.ttc"};
|
||||||
for (String path : paths) {
|
for (String path : paths) {
|
||||||
File file = new File(path);
|
File file = new File(path);
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
return PDType0Font.load(document, file);
|
return PDType0Font.load(document, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IOException("未找到中文字体文件");
|
throw new IOException("未找到中文字体文件");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+367
@@ -0,0 +1,367 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.tour.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.param.PageForm;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Sqls;
|
||||||
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.json.Json;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 疗休养人员名单统计查询控制器。
|
||||||
|
* 名单表保存可参与人员基础信息,报名线路、成团等信息通过台账补充读取。
|
||||||
|
*/
|
||||||
|
@IocBean
|
||||||
|
@Ok("json:full")
|
||||||
|
@At("/platform/tour/participantRoster")
|
||||||
|
public class TourParticipantRosterController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private TourParticipantRosterService tourParticipantRosterService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开 PC 端疗休养人员名单查询页面。
|
||||||
|
*/
|
||||||
|
@At("")
|
||||||
|
@Ok("beetl:/platform/zhgh/dayofficework/Tour/participantRoster/index.html")
|
||||||
|
@SaCheckPermission("tour.participantRoster")
|
||||||
|
public void index() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按年度范围和可选报名维度查询人员名单。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("tour.participantRoster")
|
||||||
|
public Result pageData(PageForm pageForm, Integer startYear, Integer endYear, String settingId, String matterId,
|
||||||
|
String keyword, String unitName, String unionId, Boolean joined, String lineType,
|
||||||
|
String groupCode, String undertakeUnionId) {
|
||||||
|
Result yearResult = checkYearRange(startYear, endYear);
|
||||||
|
if (yearResult != null) {
|
||||||
|
return yearResult;
|
||||||
|
}
|
||||||
|
Cnd cnd = buildQueryCnd(startYear, endYear, settingId, matterId, keyword, unitName, unionId, joined,
|
||||||
|
lineType, groupCode, undertakeUnionId);
|
||||||
|
|
||||||
|
Sql countSql = Sqls.create("""
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM tour_participant_roster r
|
||||||
|
INNER JOIN tour_setting s ON s.id = r.settingId AND s.delFlag = 0
|
||||||
|
LEFT JOIN tour_ledger t ON t.id = r.ledgerId AND t.delFlag = 0
|
||||||
|
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
||||||
|
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
|
||||||
|
$condition
|
||||||
|
""");
|
||||||
|
countSql.setCondition(cnd);
|
||||||
|
countSql.setCallback(Sqls.callback.integer());
|
||||||
|
tourParticipantRosterService.dao().execute(countSql);
|
||||||
|
|
||||||
|
Sql listSql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
s.`year`,
|
||||||
|
s.configName,
|
||||||
|
r.jobNo,
|
||||||
|
r.userName,
|
||||||
|
r.unitId,
|
||||||
|
r.unitName,
|
||||||
|
r.unionId,
|
||||||
|
r.unionName,
|
||||||
|
r.joined,
|
||||||
|
r.ledgerId,
|
||||||
|
t.lineType,
|
||||||
|
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS lineName,
|
||||||
|
m.matterName,
|
||||||
|
CASE
|
||||||
|
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
||||||
|
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
||||||
|
ELSE ''
|
||||||
|
END AS travelPeriod,
|
||||||
|
t.groupCode,
|
||||||
|
t.groupName,
|
||||||
|
t.undertakeUnionId,
|
||||||
|
t.undertakeUnionName,
|
||||||
|
t.signupTime
|
||||||
|
FROM tour_participant_roster r
|
||||||
|
INNER JOIN tour_setting s ON s.id = r.settingId AND s.delFlag = 0
|
||||||
|
LEFT JOIN tour_ledger t ON t.id = r.ledgerId AND t.delFlag = 0
|
||||||
|
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
||||||
|
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
|
||||||
|
$condition
|
||||||
|
ORDER BY $orderColumn $orderBy, s.`year` DESC, r.unionName ASC, r.unitName ASC, r.jobNo ASC
|
||||||
|
""");
|
||||||
|
listSql.setCondition(cnd);
|
||||||
|
listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName()));
|
||||||
|
listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
|
||||||
|
listSql.setPager(tourParticipantRosterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||||
|
listSql.setCallback(Sqls.callback.maps());
|
||||||
|
tourParticipantRosterService.dao().execute(listSql);
|
||||||
|
|
||||||
|
Pagination<NutMap> pagination = new Pagination<>(
|
||||||
|
pageForm.getPageNumber(),
|
||||||
|
pageForm.getPageSize(),
|
||||||
|
countSql.getInt(),
|
||||||
|
listSql.getList(NutMap.class)
|
||||||
|
);
|
||||||
|
return Result.success(pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据年度范围加载疗休养配置下拉选项。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("tour.participantRoster")
|
||||||
|
public Result settingOptions(Integer startYear, Integer endYear) {
|
||||||
|
Result yearResult = checkYearRange(startYear, endYear);
|
||||||
|
if (yearResult != null) {
|
||||||
|
return yearResult;
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT id, configName, `year`
|
||||||
|
FROM tour_setting
|
||||||
|
WHERE delFlag = 0
|
||||||
|
AND `year` >= @startYear
|
||||||
|
AND `year` <= @endYear
|
||||||
|
ORDER BY `year` DESC, sortNo ASC, createdAt DESC
|
||||||
|
""");
|
||||||
|
sql.setParam("startYear", startYear);
|
||||||
|
sql.setParam("endYear", endYear);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
tourParticipantRosterService.dao().execute(sql);
|
||||||
|
return Result.success(sql.getList(NutMap.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载线路选择器使用的疗休养事项选项。
|
||||||
|
* 未选择配置时加载年度范围内全部事项,选择配置后只加载当前配置下事项。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("tour.participantRoster")
|
||||||
|
public Result lineOptions(Integer startYear, Integer endYear, String settingId) {
|
||||||
|
Result yearResult = checkYearRange(startYear, endYear);
|
||||||
|
if (yearResult != null) {
|
||||||
|
return yearResult;
|
||||||
|
}
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
cnd.and("s.delFlag", "=", false);
|
||||||
|
cnd.and("m.delFlag", "=", false);
|
||||||
|
cnd.and("m.lineId", "IS NOT", null);
|
||||||
|
cnd.and("m.lineId", "<>", "");
|
||||||
|
cnd.and("s.`year`", ">=", startYear);
|
||||||
|
cnd.and("s.`year`", "<=", endYear);
|
||||||
|
cnd.andEX("s.id", "=", settingId);
|
||||||
|
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
m.id AS matterId,
|
||||||
|
m.matterName,
|
||||||
|
s.id AS settingId,
|
||||||
|
s.configName,
|
||||||
|
s.`year`,
|
||||||
|
l.id AS lineId,
|
||||||
|
l.lineName,
|
||||||
|
l.lineType,
|
||||||
|
CONCAT(s.configName, '【', s.`year`, '】 / ', m.matterName) AS label
|
||||||
|
FROM tour_matter m
|
||||||
|
INNER JOIN tour_setting s ON s.id = m.settingId
|
||||||
|
LEFT JOIN tour_line l ON l.id = m.lineId AND l.delFlag = 0
|
||||||
|
$condition
|
||||||
|
ORDER BY s.`year` DESC, s.sortNo ASC, m.travelStartTime ASC, m.createdAt DESC
|
||||||
|
""");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
tourParticipantRosterService.dao().execute(sql);
|
||||||
|
return Result.success(sql.getList(NutMap.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从年度范围内的人员名单加载工会下拉选项。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("tour.participantRoster")
|
||||||
|
public Result unionOptions(Integer startYear, Integer endYear) {
|
||||||
|
Result yearResult = checkYearRange(startYear, endYear);
|
||||||
|
if (yearResult != null) {
|
||||||
|
return yearResult;
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT DISTINCT r.unionId AS id, r.unionName AS name
|
||||||
|
FROM tour_participant_roster r
|
||||||
|
INNER JOIN tour_setting s ON s.id = r.settingId AND s.delFlag = 0
|
||||||
|
WHERE r.delFlag = 0
|
||||||
|
AND IFNULL(r.unionId, '') <> ''
|
||||||
|
AND s.`year` >= @startYear
|
||||||
|
AND s.`year` <= @endYear
|
||||||
|
ORDER BY r.unionName ASC
|
||||||
|
""");
|
||||||
|
sql.setParam("startYear", startYear);
|
||||||
|
sql.setParam("endYear", endYear);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
tourParticipantRosterService.dao().execute(sql);
|
||||||
|
return Result.success(sql.getList(NutMap.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据参与人员范围和现有报名台账重新生成所选配置的人员名单。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("tour.participantRoster")
|
||||||
|
public Result rebuildRoster(Integer startYear, Integer endYear, @Param("settingIds") String settingIds) {
|
||||||
|
Result yearResult = checkYearRange(startYear, endYear);
|
||||||
|
if (yearResult != null) {
|
||||||
|
return yearResult;
|
||||||
|
}
|
||||||
|
List<String> settingIdList = parseSettingIds(settingIds);
|
||||||
|
if (settingIdList.isEmpty()) {
|
||||||
|
return Result.error("请选择需要重新生成的疗休养配置");
|
||||||
|
}
|
||||||
|
Result settingResult = checkSettingsInYearRange(startYear, endYear, settingIdList);
|
||||||
|
if (settingResult != null) {
|
||||||
|
return settingResult;
|
||||||
|
}
|
||||||
|
List<String> warningMessages = tourParticipantRosterService.rebuildSettingRosters(settingIdList);
|
||||||
|
NutMap data = NutMap.NEW().addv("warnings", warningMessages);
|
||||||
|
return Result.success(data).addMsg("人员名单重新生成成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验年度范围是否完整且顺序合法。
|
||||||
|
*/
|
||||||
|
private Result checkYearRange(Integer startYear, Integer endYear) {
|
||||||
|
if (startYear == null || endYear == null) {
|
||||||
|
return Result.error("年度范围不能为空");
|
||||||
|
}
|
||||||
|
if (startYear > endYear) {
|
||||||
|
return Result.error("开始年度不能大于结束年度");
|
||||||
|
}
|
||||||
|
int nextYear = LocalDate.now().getYear() + 1;
|
||||||
|
if (startYear < 1900 || endYear > nextYear) {
|
||||||
|
return Result.error("年度范围不合法");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析前端多选配置 ID,并过滤空值和重复值。
|
||||||
|
*/
|
||||||
|
private List<String> parseSettingIds(String settingIds) {
|
||||||
|
if (StrUtil.isBlank(settingIds)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<String> rawList = Json.fromJsonAsList(String.class, settingIds);
|
||||||
|
Set<String> idSet = new LinkedHashSet<>();
|
||||||
|
for (String id : rawList) {
|
||||||
|
if (StrUtil.isNotBlank(id)) {
|
||||||
|
idSet.add(id.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(idSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验所选疗休养配置是否仍属于页面提交的年度范围。
|
||||||
|
*/
|
||||||
|
private Result checkSettingsInYearRange(Integer startYear, Integer endYear, List<String> settingIds) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM tour_setting
|
||||||
|
WHERE delFlag = 0
|
||||||
|
AND id IN (@settingIds)
|
||||||
|
AND `year` >= @startYear
|
||||||
|
AND `year` <= @endYear
|
||||||
|
""");
|
||||||
|
sql.setParam("settingIds", settingIds.toArray(new String[0]));
|
||||||
|
sql.setParam("startYear", startYear);
|
||||||
|
sql.setParam("endYear", endYear);
|
||||||
|
sql.setCallback(Sqls.callback.integer());
|
||||||
|
tourParticipantRosterService.dao().execute(sql);
|
||||||
|
if (sql.getInt() != settingIds.size()) {
|
||||||
|
return Result.error("所选疗休养配置与年度范围不匹配,请刷新后重试");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造总数查询和列表查询共用的 SQL 条件。
|
||||||
|
*/
|
||||||
|
private Cnd buildQueryCnd(Integer startYear, Integer endYear, String settingId, String matterId, String keyword,
|
||||||
|
String unitName, String unionId, Boolean joined, String lineType,
|
||||||
|
String groupCode, String undertakeUnionId) {
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
cnd.and("r.delFlag", "=", false);
|
||||||
|
cnd.and("s.`year`", ">=", startYear);
|
||||||
|
cnd.and("s.`year`", "<=", endYear);
|
||||||
|
cnd.andEX("r.settingId", "=", settingId);
|
||||||
|
cnd.andEX("t.matterId", "=", matterId);
|
||||||
|
cnd.andEX("r.unionId", "=", unionId);
|
||||||
|
cnd.andEX("r.unitName", "like", StrUtil.isBlank(unitName) ? null : "%" + unitName.trim() + "%");
|
||||||
|
cnd.andEX("t.lineType", "=", lineType);
|
||||||
|
cnd.andEX("t.undertakeUnionId", "=", undertakeUnionId);
|
||||||
|
appendKeywordFilter(cnd, keyword);
|
||||||
|
return cnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加工号和姓名模糊查询条件。
|
||||||
|
*/
|
||||||
|
private void appendKeywordFilter(Cnd cnd, String keyword) {
|
||||||
|
if (StrUtil.isBlank(keyword)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SqlExpressionGroup group = new SqlExpressionGroup();
|
||||||
|
group.orLike("r.jobNo", keyword.trim());
|
||||||
|
group.orLike("r.userName", keyword.trim());
|
||||||
|
cnd.and(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将前端排序字段映射为安全的 SQL 排序列。
|
||||||
|
*/
|
||||||
|
private String getOrderColumn(String orderName) {
|
||||||
|
if ("year".equals(orderName)) {
|
||||||
|
return "s.`year`";
|
||||||
|
}
|
||||||
|
if ("jobNo".equals(orderName)) {
|
||||||
|
return "r.jobNo";
|
||||||
|
}
|
||||||
|
if ("userName".equals(orderName)) {
|
||||||
|
return "r.userName";
|
||||||
|
}
|
||||||
|
if ("unitName".equals(orderName)) {
|
||||||
|
return "r.unitName";
|
||||||
|
}
|
||||||
|
if ("unionName".equals(orderName)) {
|
||||||
|
return "r.unionName";
|
||||||
|
}
|
||||||
|
if ("joined".equals(orderName)) {
|
||||||
|
return "r.joined";
|
||||||
|
}
|
||||||
|
if ("lineType".equals(orderName)) {
|
||||||
|
return "t.lineType";
|
||||||
|
}
|
||||||
|
if ("lineName".equals(orderName)) {
|
||||||
|
return "COALESCE(NULLIF(l.lineName, ''), t.lineName)";
|
||||||
|
}
|
||||||
|
if ("signupTime".equals(orderName)) {
|
||||||
|
return "t.signupTime";
|
||||||
|
}
|
||||||
|
return "s.`year`";
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-24
@@ -97,18 +97,7 @@ public class TourSchoolUnionApprovalController {
|
|||||||
task.taskParentId,
|
task.taskParentId,
|
||||||
task.variable AS taskVariable,
|
task.variable AS taskVariable,
|
||||||
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName,
|
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName,
|
||||||
CASE
|
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName,
|
||||||
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
|
|
||||||
ELSE CONCAT(
|
|
||||||
COALESCE(NULLIF(l.lineName, ''), t.lineName),
|
|
||||||
'(',
|
|
||||||
CASE
|
|
||||||
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
|
|
||||||
ELSE IFNULL(mu.name, '')
|
|
||||||
END,
|
|
||||||
')'
|
|
||||||
)
|
|
||||||
END AS currentLineName,
|
|
||||||
CASE
|
CASE
|
||||||
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
||||||
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
||||||
@@ -479,18 +468,7 @@ public class TourSchoolUnionApprovalController {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT CASE
|
SELECT COALESCE(NULLIF(l.lineName, ''), t.lineName) AS lineName
|
||||||
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
|
|
||||||
ELSE CONCAT(
|
|
||||||
COALESCE(NULLIF(l.lineName, ''), t.lineName),
|
|
||||||
'(',
|
|
||||||
CASE
|
|
||||||
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
|
|
||||||
ELSE IFNULL(mu.name, '')
|
|
||||||
END,
|
|
||||||
')'
|
|
||||||
)
|
|
||||||
END AS lineName
|
|
||||||
FROM tour_ledger t
|
FROM tour_ledger t
|
||||||
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
||||||
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
|
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
|
||||||
|
|||||||
+12
-9
@@ -8,6 +8,7 @@ import com.budwk.app.base.param.PageForm;
|
|||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingLot;
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingLot;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
@@ -32,6 +33,9 @@ public class TourSettingController {
|
|||||||
@Inject
|
@Inject
|
||||||
private TourSettingService tourSettingService;
|
private TourSettingService tourSettingService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private TourParticipantRosterService tourParticipantRosterService;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("beetl:/platform/zhgh/dayofficework/Tour/setting/index.html")
|
@Ok("beetl:/platform/zhgh/dayofficework/Tour/setting/index.html")
|
||||||
@SaCheckPermission("tour.setting")
|
@SaCheckPermission("tour.setting")
|
||||||
@@ -153,9 +157,11 @@ public class TourSettingController {
|
|||||||
if (Lang.isNotEmpty(lotDeleteList)) {
|
if (Lang.isNotEmpty(lotDeleteList)) {
|
||||||
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getId, "in", lotDeleteList));
|
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getId, "in", lotDeleteList));
|
||||||
}
|
}
|
||||||
tourSettingService.updateIgnoreNull(tourSetting);
|
tourSettingService.updateWithNullableSignupTime(tourSetting);
|
||||||
saveLots(tourSetting);
|
saveLots(tourSetting);
|
||||||
}
|
}
|
||||||
|
// Initialize the participant roster only once; existing roster data is preserved on later setting edits.
|
||||||
|
tourParticipantRosterService.syncSettingRosterIfEmpty(tourSetting.getId());
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +174,8 @@ public class TourSettingController {
|
|||||||
return Result.error("参数错误");
|
return Result.error("参数错误");
|
||||||
}
|
}
|
||||||
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id));
|
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id));
|
||||||
|
// Remove roster rows for the deleted setting so later statistics ignore obsolete data.
|
||||||
|
tourParticipantRosterService.clearSettingRoster(id);
|
||||||
tourSettingService.delete(id);
|
tourSettingService.delete(id);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
@@ -205,14 +213,9 @@ public class TourSettingController {
|
|||||||
if (StrUtil.isBlank(tourSetting.getConfigName())) {
|
if (StrUtil.isBlank(tourSetting.getConfigName())) {
|
||||||
return Result.error("配置名称不能为空");
|
return Result.error("配置名称不能为空");
|
||||||
}
|
}
|
||||||
if (tourSetting.getMinGroupPeople() != null && tourSetting.getMaxGroupPeople() != null
|
// 报名时间允许不配置;仅在开始和结束都填写时校验先后顺序。
|
||||||
&& tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) {
|
if (StrUtil.isNotBlank(tourSetting.getSignupStartTime()) && StrUtil.isNotBlank(tourSetting.getSignupEndTime())
|
||||||
return Result.error("最少成团人数不能大于最多成团人数");
|
&& tourSetting.getSignupStartTime().compareTo(tourSetting.getSignupEndTime()) >= 0) {
|
||||||
}
|
|
||||||
if (StrUtil.isBlank(tourSetting.getSignupStartTime()) || StrUtil.isBlank(tourSetting.getSignupEndTime())) {
|
|
||||||
return Result.error("报名时间不能为空");
|
|
||||||
}
|
|
||||||
if (tourSetting.getSignupStartTime().compareTo(tourSetting.getSignupEndTime()) >= 0) {
|
|
||||||
return Result.error("开始报名时间必须小于结束报名时间");
|
return Result.error("开始报名时间必须小于结束报名时间");
|
||||||
}
|
}
|
||||||
if (tourSetting.getCycleStartYear() != null && tourSetting.getCycleEndYear() != null
|
if (tourSetting.getCycleStartYear() != null && tourSetting.getCycleEndYear() != null
|
||||||
|
|||||||
+201
-105
@@ -1,5 +1,9 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.tour.controller;
|
package com.budwk.app.zhgh.dayofficework.tour.controller;
|
||||||
|
|
||||||
|
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.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.dev33.satoken.annotation.SaMode;
|
import cn.dev33.satoken.annotation.SaMode;
|
||||||
import cn.hutool.core.lang.Dict;
|
import cn.hutool.core.lang.Dict;
|
||||||
@@ -7,6 +11,7 @@ import cn.hutool.core.util.StrUtil;
|
|||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
import com.budwk.app.flow.constant.FlowConst;
|
import com.budwk.app.flow.constant.FlowConst;
|
||||||
import com.budwk.app.flow.engine.FlowEngine;
|
import com.budwk.app.flow.engine.FlowEngine;
|
||||||
import com.budwk.app.flow.entity.ProcessInstance;
|
import com.budwk.app.flow.entity.ProcessInstance;
|
||||||
@@ -30,6 +35,7 @@ import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeSer
|
|||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
@@ -44,10 +50,13 @@ import org.nutz.lang.util.NutMap;
|
|||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -71,6 +80,9 @@ public class TourSignupController {
|
|||||||
@Inject
|
@Inject
|
||||||
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
|
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private TourParticipantRosterService tourParticipantRosterService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private SysDictService sysDictService;
|
private SysDictService sysDictService;
|
||||||
|
|
||||||
@@ -136,7 +148,7 @@ public class TourSignupController {
|
|||||||
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
|
||||||
public Result h5PageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId, Boolean directFamilyUnitLine) {
|
public Result h5PageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId, Boolean directFamilyUnitLine) {
|
||||||
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
||||||
pageForm.defaultSortAsc("lineName");
|
pageForm.defaultSortAsc("lineCode");
|
||||||
}
|
}
|
||||||
Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId);
|
Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId);
|
||||||
cnd.andEX("l.directFamilyUnitLine", "=", directFamilyUnitLine);
|
cnd.andEX("l.directFamilyUnitLine", "=", directFamilyUnitLine);
|
||||||
@@ -171,6 +183,9 @@ public class TourSignupController {
|
|||||||
lot.lotName,
|
lot.lotName,
|
||||||
m.travelStartTime,
|
m.travelStartTime,
|
||||||
m.travelEndTime,
|
m.travelEndTime,
|
||||||
|
COALESCE(NULLIF(s.signupStartTime, ''), m.signupStartTime) AS signupStartTime,
|
||||||
|
COALESCE(NULLIF(s.signupEndTime, ''), m.signupEndTime) AS signupEndTime,
|
||||||
|
m.maxSignupPeople,
|
||||||
IFNULL(sc.signupCount, 0) AS signupCount,
|
IFNULL(sc.signupCount, 0) AS signupCount,
|
||||||
IFNULL(sc.familyCount, 0) AS familyCount,
|
IFNULL(sc.familyCount, 0) AS familyCount,
|
||||||
my.ledgerId,
|
my.ledgerId,
|
||||||
@@ -183,6 +198,7 @@ public class TourSignupController {
|
|||||||
FROM tour_matter m
|
FROM tour_matter m
|
||||||
INNER JOIN tour_line l ON l.id = m.lineId
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
LEFT JOIN sys_union u ON u.id = m.unionId
|
LEFT JOIN sys_union u ON u.id = m.unionId
|
||||||
|
LEFT JOIN tour_setting s ON s.id = m.settingId
|
||||||
LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId
|
LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId
|
||||||
LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId
|
LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
@@ -222,7 +238,7 @@ public class TourSignupController {
|
|||||||
GROUP BY t.matterId, t.jobNo
|
GROUP BY t.matterId, t.jobNo
|
||||||
) my ON my.matterId = m.id
|
) my ON my.matterId = m.id
|
||||||
$condition
|
$condition
|
||||||
ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC
|
ORDER BY $orderColumn $orderBy, CAST(l.lineCode AS UNSIGNED) ASC, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC
|
||||||
""");
|
""");
|
||||||
listSql.setCondition(cnd);
|
listSql.setCondition(cnd);
|
||||||
listSql.setParam("currentJobNo", currentJobNo);
|
listSql.setParam("currentJobNo", currentJobNo);
|
||||||
@@ -241,7 +257,7 @@ public class TourSignupController {
|
|||||||
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
|
||||||
public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId) {
|
public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId) {
|
||||||
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
||||||
pageForm.defaultSortAsc("lineName");
|
pageForm.defaultSortAsc("lineCode");
|
||||||
}
|
}
|
||||||
Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId);
|
Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId);
|
||||||
String currentJobNo = currentJobNo();
|
String currentJobNo = currentJobNo();
|
||||||
@@ -265,6 +281,7 @@ public class TourSignupController {
|
|||||||
m.matterName,
|
m.matterName,
|
||||||
m.minGroupPeople,
|
m.minGroupPeople,
|
||||||
m.maxGroupPeople,
|
m.maxGroupPeople,
|
||||||
|
m.maxSignupPeople,
|
||||||
m.unionId,
|
m.unionId,
|
||||||
COALESCE(u.name, '校工会') AS unionName,
|
COALESCE(u.name, '校工会') AS unionName,
|
||||||
m.lineId,
|
m.lineId,
|
||||||
@@ -325,12 +342,12 @@ public class TourSignupController {
|
|||||||
GROUP BY t.matterId, t.jobNo
|
GROUP BY t.matterId, t.jobNo
|
||||||
) my ON my.matterId = m.id
|
) my ON my.matterId = m.id
|
||||||
$condition
|
$condition
|
||||||
ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC
|
ORDER BY $orderColumn $orderBy, CAST(l.lineCode AS UNSIGNED) ASC, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC
|
||||||
""");
|
""");
|
||||||
listSql.setCondition(cnd);
|
listSql.setCondition(cnd);
|
||||||
listSql.setParam("currentJobNo", currentJobNo);
|
listSql.setParam("currentJobNo", currentJobNo);
|
||||||
listSql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode());
|
listSql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode());
|
||||||
listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName()));
|
listSql.setVar("orderColumn", tourLedgerService.getTourSignupOrderColumn(pageForm.getPageOrderName()));
|
||||||
listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
|
listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
|
||||||
listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||||
listSql.setCallback(Sqls.callback.maps());
|
listSql.setCallback(Sqls.callback.maps());
|
||||||
@@ -340,6 +357,28 @@ public class TourSignupController {
|
|||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 PC 端疗休养报名列表,导出范围与当前页面查询条件保持一致。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("tour.signup")
|
||||||
|
@Ok("void")
|
||||||
|
public void exportData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
||||||
|
pageForm.defaultSortAsc("lineCode");
|
||||||
|
}
|
||||||
|
// 导出不分页,按当前查询条件和排序口径查询全量列表数据。
|
||||||
|
List<NutMap> rows = querySignupExportRows(pageForm, year, lineName, lineType, unionId);
|
||||||
|
List<ExcelExportEntity> entities = buildSignupExportEntities();
|
||||||
|
|
||||||
|
// 导出文件统一使用 XSSF,避免大数据量时生成旧版 xls 格式。
|
||||||
|
ExportParams exportParams = new ExportParams();
|
||||||
|
exportParams.setType(ExcelType.XSSF);
|
||||||
|
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, rows);
|
||||||
|
CommonDownloadUtil.download("疗休养报名.xlsx", workbook, response);
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
|
||||||
public Result unionOptions() {
|
public Result unionOptions() {
|
||||||
@@ -464,6 +503,7 @@ public class TourSignupController {
|
|||||||
m.travelStartTime,
|
m.travelStartTime,
|
||||||
m.travelEndTime,
|
m.travelEndTime,
|
||||||
m.contactName,
|
m.contactName,
|
||||||
|
m.maxSignupPeople,
|
||||||
CASE
|
CASE
|
||||||
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
||||||
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
||||||
@@ -529,19 +569,6 @@ public class TourSignupController {
|
|||||||
.and(TourLedger::getJobNo, "=", staff.getString("jobNo"));
|
.and(TourLedger::getJobNo, "=", staff.getString("jobNo"));
|
||||||
ledgerCnd.desc(TourLedger::getCreatedAt);
|
ledgerCnd.desc(TourLedger::getCreatedAt);
|
||||||
TourLedger ledger = tourLedgerService.fetch(ledgerCnd);
|
TourLedger ledger = tourLedgerService.fetch(ledgerCnd);
|
||||||
TourMatter matterEntity = tourMatterService.fetch(matter.getString("matterId"));
|
|
||||||
TourLedger ruleLedger = new TourLedger();
|
|
||||||
ruleLedger.setId(ledger == null ? null : ledger.getId());
|
|
||||||
ruleLedger.setYear(matterEntity == null ? matter.getInt("year") : matterEntity.getYear());
|
|
||||||
ruleLedger.setMatterId(matter.getString("matterId"));
|
|
||||||
ruleLedger.setLineId(matter.getString("lineId"));
|
|
||||||
ruleLedger.setLineName(matter.getString("lineName"));
|
|
||||||
if (!Boolean.TRUE.equals(noticeChecked)) {
|
|
||||||
Result ruleResult = checkSignupRule(ruleLedger, matterEntity, ledger);
|
|
||||||
if (ruleResult != null) {
|
|
||||||
return ruleResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<TourLedgerFamily> families = Collections.emptyList();
|
List<TourLedgerFamily> families = Collections.emptyList();
|
||||||
TourLedgerDirectRelative directRelative = null;
|
TourLedgerDirectRelative directRelative = null;
|
||||||
if (ledger != null) {
|
if (ledger != null) {
|
||||||
@@ -583,48 +610,9 @@ public class TourSignupController {
|
|||||||
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
|
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
|
||||||
return Result.error("您不在本次疗休养报名范围内");
|
return Result.error("您不在本次疗休养报名范围内");
|
||||||
}
|
}
|
||||||
TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId());
|
// 我要报名入口不再做周期次数、周期费用和省外名额提示,最终报名规则由 doSignup 统一校验。
|
||||||
CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger);
|
|
||||||
if (!cycleAllowedTimes.canApply()) {
|
|
||||||
return Result.success(NutMap.NEW()
|
|
||||||
.addv("canApply", false)
|
|
||||||
.addv("noticeRequired", true)
|
|
||||||
.addv("noticeType", "cycleAllowedTimes")
|
|
||||||
.addv("cycleStartYear", cycleAllowedTimes.startYear())
|
|
||||||
.addv("cycleEndYear", cycleAllowedTimes.endYear())
|
|
||||||
.addv("cycleAllowedTimes", cycleAllowedTimes.allowedTimes())
|
|
||||||
.addv("cycleJoinedTimes", cycleAllowedTimes.joinedTimes())
|
|
||||||
.addv("message", buildCycleAllowedTimesNotice(cycleAllowedTimes)));
|
|
||||||
}
|
|
||||||
CycleTotalCost cycleTotalCost = calculateCycleTotalCost(matter, setting, line, oldLedger);
|
|
||||||
if (!cycleTotalCost.canApply()) {
|
|
||||||
return Result.success(NutMap.NEW()
|
|
||||||
.addv("canApply", false)
|
|
||||||
.addv("noticeRequired", true)
|
|
||||||
.addv("noticeType", "cycleTotalCost")
|
|
||||||
.addv("cycleStartYear", cycleTotalCost.startYear())
|
|
||||||
.addv("cycleEndYear", cycleTotalCost.endYear())
|
|
||||||
.addv("cycleTotalCost", cycleTotalCost.totalCost())
|
|
||||||
.addv("cycleJoinedCost", cycleTotalCost.joinedCost())
|
|
||||||
.addv("currentLineCost", cycleTotalCost.currentCost())
|
|
||||||
.addv("message", buildCycleTotalCostNotice(cycleTotalCost)));
|
|
||||||
}
|
|
||||||
if (!isOutProvinceLine(line.getLineType())) {
|
|
||||||
return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false));
|
return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false));
|
||||||
}
|
}
|
||||||
OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger);
|
|
||||||
return Result.success(NutMap.NEW()
|
|
||||||
.addv("noticeRequired", true)
|
|
||||||
.addv("noticeType", "outProvinceQuota")
|
|
||||||
.addv("canApply", quota.canApply())
|
|
||||||
.addv("ratioType", quota.ratioType())
|
|
||||||
.addv("basePeople", quota.basePeople())
|
|
||||||
.addv("totalSignupPeople", quota.totalSignupPeople())
|
|
||||||
.addv("outProvinceSignupPeople", quota.outProvinceSignupPeople())
|
|
||||||
.addv("allowPeople", quota.allowPeople())
|
|
||||||
.addv("ratioText", quota.ratioText())
|
|
||||||
.addv("message", buildOutProvinceQuotaNotice(quota)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
|
||||||
@@ -740,7 +728,8 @@ public class TourSignupController {
|
|||||||
return Result.error("不能修改他人的报名记录");
|
return Result.error("不能修改他人的报名记录");
|
||||||
}
|
}
|
||||||
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
|
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
|
||||||
Result ruleResult = checkSignupRule(ledger, matter, update ? oldLedger : null);
|
TourLedger ruleOldLedger = update ? oldLedger : fetchCurrentUserMatterLedger(ledger.getMatterId());
|
||||||
|
Result ruleResult = checkSignupRule(ledger, matter, update ? oldLedger : null, ruleOldLedger, calculateCurrentSignupPeople(familyList));
|
||||||
if (ruleResult != null) {
|
if (ruleResult != null) {
|
||||||
return ruleResult;
|
return ruleResult;
|
||||||
}
|
}
|
||||||
@@ -751,10 +740,6 @@ public class TourSignupController {
|
|||||||
update = true;
|
update = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Result maxPeopleResult = checkMaxGroupPeople(matter, oldLedger, familyList);
|
|
||||||
if (maxPeopleResult != null) {
|
|
||||||
return maxPeopleResult;
|
|
||||||
}
|
|
||||||
TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId());
|
TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId());
|
||||||
boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine());
|
boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine());
|
||||||
TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId());
|
TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId());
|
||||||
@@ -817,6 +802,8 @@ public class TourSignupController {
|
|||||||
return workflowResult;
|
return workflowResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Signup and modification both mean the staff member is counted as joined for this setting.
|
||||||
|
tourParticipantRosterService.markJoinedByLedger(ledger);
|
||||||
return Result.success().addMsg(approvalRequired ? (update ? "修改已提交,等待审核" : "申请已提交,等待审核") : (update ? "修改成功" : "报名成功"));
|
return Result.success().addMsg(approvalRequired ? (update ? "修改已提交,等待审核" : "申请已提交,等待审核") : (update ? "修改成功" : "报名成功"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -841,6 +828,14 @@ public class TourSignupController {
|
|||||||
if (!currentJobNo().equals(ledger.getJobNo())) {
|
if (!currentJobNo().equals(ledger.getJobNo())) {
|
||||||
return Result.error("不能取消他人的报名记录");
|
return Result.error("不能取消他人的报名记录");
|
||||||
}
|
}
|
||||||
|
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
|
||||||
|
TourSetting setting = matter == null ? null : tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId());
|
||||||
|
Result signupTimeResult = tourLedgerService.checkSignupTimeWindow(matter, setting);
|
||||||
|
if (signupTimeResult != null) {
|
||||||
|
return signupTimeResult;
|
||||||
|
}
|
||||||
|
// Reset the roster before deleting the ledger record, because the ledger still carries setting linkage here.
|
||||||
|
tourParticipantRosterService.markNotJoinedByLedger(ledger);
|
||||||
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
|
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
|
||||||
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
|
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
|
||||||
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
|
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
|
||||||
@@ -962,26 +957,12 @@ public class TourSignupController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result checkMaxGroupPeople(TourMatter matter, TourLedger oldLedger, List<TourLedgerFamily> familyList) {
|
|
||||||
if (matter == null || matter.getMaxGroupPeople() == null || matter.getMaxGroupPeople() <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int currentPeople = 1 + (Lang.isEmpty(familyList) ? 0 : familyList.size());
|
|
||||||
int signedPeople = countSignupPeople(matter.getId(), oldLedger == null ? null : oldLedger.getId());
|
|
||||||
int totalPeople = signedPeople + currentPeople;
|
|
||||||
if (totalPeople > matter.getMaxGroupPeople()) {
|
|
||||||
return Result.error("当前报名人数 " + signedPeople + " 人,本次报名 " + currentPeople
|
|
||||||
+ " 人,最多成团人数 " + matter.getMaxGroupPeople() + " 人,报名后将超过人数上限,不能提交");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isValidIdCard(String idCard) {
|
private boolean isValidIdCard(String idCard) {
|
||||||
return StrUtil.isNotBlank(idCard)
|
return StrUtil.isNotBlank(idCard)
|
||||||
&& idCard.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dX]$");
|
&& idCard.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dX]$");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result checkSignupRule(TourLedger ledger, TourMatter matter, TourLedger oldLedger) {
|
private Result checkSignupRule(TourLedger ledger, TourMatter matter, TourLedger oldLedger, TourLedger maxGroupOldLedger, int currentPeopleCount) {
|
||||||
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) {
|
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) {
|
||||||
return Result.error("报名出行时段不存在或已停用");
|
return Result.error("报名出行时段不存在或已停用");
|
||||||
}
|
}
|
||||||
@@ -996,6 +977,10 @@ public class TourSignupController {
|
|||||||
if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) {
|
if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) {
|
||||||
return Result.error("报名事项配置不存在或已停用");
|
return Result.error("报名事项配置不存在或已停用");
|
||||||
}
|
}
|
||||||
|
Result maxSignupResult = checkMaxSignupPeople(matter, maxGroupOldLedger, currentPeopleCount);
|
||||||
|
if (maxSignupResult != null) {
|
||||||
|
return maxSignupResult;
|
||||||
|
}
|
||||||
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
|
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
|
||||||
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
|
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
|
||||||
return Result.error("您不在本次疗休养报名范围内");
|
return Result.error("您不在本次疗休养报名范围内");
|
||||||
@@ -1035,6 +1020,8 @@ public class TourSignupController {
|
|||||||
if (tourLedgerService.count(outProvinceCnd) > 0) {
|
if (tourLedgerService.count(outProvinceCnd) > 0) {
|
||||||
return Result.error("您已参加过省外线路,不能报名省外线路,请选择省内线路");
|
return Result.error("您已参加过省外线路,不能报名省外线路,请选择省内线路");
|
||||||
}
|
}
|
||||||
|
// Out-province quota is shared by year; lock only before the quota count calculation.
|
||||||
|
tourLedgerService.lockTourSignupQuotaYear(matter.getYear());
|
||||||
OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger);
|
OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger);
|
||||||
if (!quota.canApply()) {
|
if (!quota.canApply()) {
|
||||||
return Result.error(buildOutProvinceQuotaNotice(quota));
|
return Result.error(buildOutProvinceQuotaNotice(quota));
|
||||||
@@ -1066,6 +1053,61 @@ public class TourSignupController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the matter maximum signup capacity at final submit time.
|
||||||
|
*/
|
||||||
|
private Result checkMaxSignupPeople(TourMatter matter, TourLedger oldLedger, int currentPeopleCount) {
|
||||||
|
int maxSignupPeople = matter == null || matter.getMaxSignupPeople() == null ? 0 : matter.getMaxSignupPeople();
|
||||||
|
if (maxSignupPeople <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Maximum signup capacity is owned by the current matter, so only the same matter is serialized.
|
||||||
|
tourLedgerService.lockTourSignupMatter(matter.getId());
|
||||||
|
String excludeLedgerId = oldLedger == null ? null : oldLedger.getId();
|
||||||
|
int signedPeople = countMatterSignupPeople(matter.getId(), excludeLedgerId);
|
||||||
|
if (signedPeople + currentPeopleCount > maxSignupPeople) {
|
||||||
|
return Result.error("当前线路最多报名人数为" + maxSignupPeople + "人,当前已报名" + signedPeople
|
||||||
|
+ "人,本次报名" + currentPeopleCount + "人,不能超过最多报名人数");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts current submit people as one staff member plus all submitted family members.
|
||||||
|
*/
|
||||||
|
private int calculateCurrentSignupPeople(List<TourLedgerFamily> familyList) {
|
||||||
|
return 1 + (familyList == null ? 0 : familyList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts valid signed people for a matter, including staff and their family members.
|
||||||
|
*/
|
||||||
|
private int countMatterSignupPeople(String matterId, String excludeLedgerId) {
|
||||||
|
if (StrUtil.isBlank(matterId)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId";
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS peopleCount
|
||||||
|
FROM tour_ledger t
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
|
FROM tour_ledger_family
|
||||||
|
WHERE delFlag = 0
|
||||||
|
GROUP BY ledgerId
|
||||||
|
) f ON f.ledgerId = t.id
|
||||||
|
WHERE t.delFlag = 0
|
||||||
|
AND t.matterId = @matterId
|
||||||
|
""" + excludeSql);
|
||||||
|
sql.setParam("matterId", matterId);
|
||||||
|
if (StrUtil.isNotBlank(excludeLedgerId)) {
|
||||||
|
sql.setParam("excludeLedgerId", excludeLedgerId);
|
||||||
|
}
|
||||||
|
sql.setCallback(Sqls.callback.integer());
|
||||||
|
tourLedgerService.dao().execute(sql);
|
||||||
|
return sql.getInt();
|
||||||
|
}
|
||||||
|
|
||||||
private TourLedger fetchLatestLedger(Cnd cnd) {
|
private TourLedger fetchLatestLedger(Cnd cnd) {
|
||||||
cnd.desc(TourLedger::getSignupTime);
|
cnd.desc(TourLedger::getSignupTime);
|
||||||
cnd.desc(TourLedger::getCreatedAt);
|
cnd.desc(TourLedger::getCreatedAt);
|
||||||
@@ -1252,32 +1294,6 @@ public class TourSignupController {
|
|||||||
return sql.getInt();
|
return sql.getInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int countSignupPeople(String matterId, String excludeLedgerId) {
|
|
||||||
if (StrUtil.isBlank(matterId)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId";
|
|
||||||
Sql sql = Sqls.create("""
|
|
||||||
SELECT IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS signupPeople
|
|
||||||
FROM tour_ledger t
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT ledgerId, COUNT(1) AS familyCount
|
|
||||||
FROM tour_ledger_family
|
|
||||||
WHERE delFlag = 0
|
|
||||||
GROUP BY ledgerId
|
|
||||||
) f ON f.ledgerId = t.id
|
|
||||||
WHERE t.delFlag = 0
|
|
||||||
AND t.matterId = @matterId
|
|
||||||
""" + excludeSql);
|
|
||||||
sql.setParam("matterId", matterId);
|
|
||||||
if (StrUtil.isNotBlank(excludeLedgerId)) {
|
|
||||||
sql.setParam("excludeLedgerId", excludeLedgerId);
|
|
||||||
}
|
|
||||||
sql.setCallback(Sqls.callback.integer());
|
|
||||||
tourLedgerService.dao().execute(sql);
|
|
||||||
return sql.getInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildSelectedLineMessage(TourLedger ledger) {
|
private String buildSelectedLineMessage(TourLedger ledger) {
|
||||||
String selectedLineName = getLedgerLineName(ledger);
|
String selectedLineName = getLedgerLineName(ledger);
|
||||||
return "您已经选择【" + selectedLineName + "】,取消后再重新选择。";
|
return "您已经选择【" + selectedLineName + "】,取消后再重新选择。";
|
||||||
@@ -1479,6 +1495,79 @@ public class TourSignupController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 PC 疗休养报名列表同一批查询条件导出全量数据,导出不分页但保留当前排序口径。
|
||||||
|
*/
|
||||||
|
private List<NutMap> querySignupExportRows(PageForm pageForm, Integer year, String lineName, String lineType, String unionId) {
|
||||||
|
Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId);
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
l.lineName,
|
||||||
|
l.lineType,
|
||||||
|
m.minGroupPeople,
|
||||||
|
m.maxGroupPeople,
|
||||||
|
m.maxSignupPeople,
|
||||||
|
CASE
|
||||||
|
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
||||||
|
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
||||||
|
ELSE ''
|
||||||
|
END AS travelPeriod,
|
||||||
|
IFNULL(sc.signupCount, 0) AS signupCount
|
||||||
|
FROM tour_matter m
|
||||||
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
LEFT JOIN sys_union u ON u.id = m.unionId
|
||||||
|
LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT t.matterId, SUM(1 + IFNULL(f.familyCount, 0)) AS signupCount
|
||||||
|
FROM tour_ledger t
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
|
FROM tour_ledger_family
|
||||||
|
WHERE delFlag = 0
|
||||||
|
GROUP BY ledgerId
|
||||||
|
) f ON f.ledgerId = t.id
|
||||||
|
WHERE t.delFlag = 0
|
||||||
|
AND t.matterId IS NOT NULL
|
||||||
|
AND t.matterId <> ''
|
||||||
|
GROUP BY t.matterId
|
||||||
|
) sc ON sc.matterId = m.id
|
||||||
|
$condition
|
||||||
|
ORDER BY $orderColumn $orderBy, CAST(l.lineCode AS UNSIGNED) ASC, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC
|
||||||
|
""");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
sql.setVar("orderColumn", tourLedgerService.getTourSignupOrderColumn(pageForm.getPageOrderName()));
|
||||||
|
sql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
tourMatterService.dao().execute(sql);
|
||||||
|
|
||||||
|
List<NutMap> rows = sql.getList(NutMap.class);
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
NutMap row = rows.get(i);
|
||||||
|
row.put("indexNo", i + 1);
|
||||||
|
row.put("minGroupPeopleText", row.getInt("minGroupPeople", 0) + "人");
|
||||||
|
row.put("maxGroupPeopleText", row.getInt("maxGroupPeople", 0) + "人");
|
||||||
|
row.put("maxSignupPeopleText", row.getInt("maxSignupPeople", 0) + "人");
|
||||||
|
row.put("signupCountText", row.getInt("signupCount", 0) + "人");
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds export columns consistent with the PC list; signup count is always included.
|
||||||
|
*/
|
||||||
|
private List<ExcelExportEntity> buildSignupExportEntities() {
|
||||||
|
List<ExcelExportEntity> entities = new ArrayList<>();
|
||||||
|
entities.add(new ExcelExportEntity("序号", "indexNo", 10));
|
||||||
|
entities.add(new ExcelExportEntity("线路名称", "lineName", 30));
|
||||||
|
entities.add(new ExcelExportEntity("出行时段", "travelPeriod", 28));
|
||||||
|
entities.add(new ExcelExportEntity("线路类型", "lineType", 16));
|
||||||
|
entities.add(new ExcelExportEntity("最少成团人数", "minGroupPeopleText", 16));
|
||||||
|
entities.add(new ExcelExportEntity("最多成团人数", "maxGroupPeopleText", 16));
|
||||||
|
entities.add(new ExcelExportEntity("最多报名人数", "maxSignupPeopleText", 16));
|
||||||
|
entities.add(new ExcelExportEntity("报名人数", "signupCountText", 16));
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
private Result checkSignupApprovalWorkflowCanSubmit(String ledgerId) {
|
private Result checkSignupApprovalWorkflowCanSubmit(String ledgerId) {
|
||||||
ProcessInstance instance = tourLedgerService.dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", ledgerId)
|
ProcessInstance instance = tourLedgerService.dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", ledgerId)
|
||||||
.and(ProcessInstance::getState, "<>", ProcessInstanceStateEnum.ABANDON.getCode())
|
.and(ProcessInstance::getState, "<>", ProcessInstanceStateEnum.ABANDON.getCode())
|
||||||
@@ -1537,6 +1626,10 @@ public class TourSignupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getOrderColumn(String orderName) {
|
private String getOrderColumn(String orderName) {
|
||||||
|
if ("lineCode".equals(orderName)) {
|
||||||
|
// 线路序号按数字排序,避免字符串排序导致 10 排在 2 前面。
|
||||||
|
return "CAST(l.lineCode AS UNSIGNED)";
|
||||||
|
}
|
||||||
if ("lineName".equals(orderName)) {
|
if ("lineName".equals(orderName)) {
|
||||||
return "l.lineName";
|
return "l.lineName";
|
||||||
}
|
}
|
||||||
@@ -1558,9 +1651,12 @@ public class TourSignupController {
|
|||||||
if ("maxGroupPeople".equals(orderName)) {
|
if ("maxGroupPeople".equals(orderName)) {
|
||||||
return "m.maxGroupPeople";
|
return "m.maxGroupPeople";
|
||||||
}
|
}
|
||||||
|
if ("maxSignupPeople".equals(orderName)) {
|
||||||
|
return "m.maxSignupPeople";
|
||||||
|
}
|
||||||
if ("signupCount".equals(orderName)) {
|
if ("signupCount".equals(orderName)) {
|
||||||
return "signupCount";
|
return "signupCount";
|
||||||
}
|
}
|
||||||
return "l.lineName";
|
return "CAST(l.lineCode AS UNSIGNED)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+298
-96
@@ -1,17 +1,22 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.tour.controller;
|
package com.budwk.app.zhgh.dayofficework.tour.controller;
|
||||||
|
|
||||||
|
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.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
import com.budwk.app.sys.models.Sys_union;
|
import com.budwk.app.sys.models.Sys_union;
|
||||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
|
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
@@ -22,6 +27,7 @@ import org.nutz.lang.util.NutMap;
|
|||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -61,7 +67,17 @@ public class TourUndertakeController {
|
|||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
FROM tour_matter m
|
FROM tour_matter m
|
||||||
INNER JOIN tour_line l ON l.id = m.lineId
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
LEFT JOIN sys_union u ON u.id = m.unionId
|
INNER JOIN (
|
||||||
|
SELECT
|
||||||
|
t.matterId,
|
||||||
|
t.groupCode,
|
||||||
|
MAX(t.undertakeUnionId) AS undertakeUnionId
|
||||||
|
FROM tour_ledger t
|
||||||
|
WHERE t.delFlag = 0
|
||||||
|
AND t.groupCode IS NOT NULL
|
||||||
|
AND t.groupCode <> ''
|
||||||
|
GROUP BY t.matterId, t.groupCode
|
||||||
|
) gs ON gs.matterId = m.id
|
||||||
$condition
|
$condition
|
||||||
""");
|
""");
|
||||||
countSql.setCondition(cnd);
|
countSql.setCondition(cnd);
|
||||||
@@ -74,14 +90,14 @@ public class TourUndertakeController {
|
|||||||
m.id AS matterId,
|
m.id AS matterId,
|
||||||
m.`year`,
|
m.`year`,
|
||||||
m.matterName,
|
m.matterName,
|
||||||
m.unionId,
|
gs.undertakeUnionId AS unionId,
|
||||||
|
COALESCE(NULLIF(gs.undertakeUnionName, ''), uu.name, '') AS unionName,
|
||||||
CASE
|
CASE
|
||||||
WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN COALESCE(u.name, '校工会')
|
WHEN IFNULL(NULLIF(gs.undertakeUnionId, ''), '') <> '' THEN 'claimed'
|
||||||
ELSE ''
|
ELSE 'unclaimed'
|
||||||
END AS unionName,
|
END AS undertakeStatus,
|
||||||
IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') AS undertakeStatus,
|
|
||||||
CASE
|
CASE
|
||||||
WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN '已领'
|
WHEN IFNULL(NULLIF(gs.undertakeUnionId, ''), '') <> '' THEN '已领'
|
||||||
ELSE '未领'
|
ELSE '未领'
|
||||||
END AS undertakeStatusName,
|
END AS undertakeStatusName,
|
||||||
m.lineId,
|
m.lineId,
|
||||||
@@ -95,25 +111,20 @@ public class TourUndertakeController {
|
|||||||
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
||||||
ELSE ''
|
ELSE ''
|
||||||
END AS travelPeriod,
|
END AS travelPeriod,
|
||||||
m.minGroupPeople,
|
gs.groupCode,
|
||||||
m.maxGroupPeople,
|
gs.groupName,
|
||||||
IFNULL(sc.signupCount, 0) AS signupCount,
|
gs.signupCount,
|
||||||
IFNULL(usc.currentUnionSignupCount, 0) AS currentUnionSignupCount,
|
IFNULL(ugs.currentUnionSignupCount, 0) AS currentUnionSignupCount
|
||||||
CASE
|
|
||||||
WHEN m.maxGroupPeople IS NOT NULL AND m.maxGroupPeople > 0 AND IFNULL(sc.signupCount, 0) > m.maxGroupPeople THEN 'over'
|
|
||||||
WHEN m.minGroupPeople IS NOT NULL AND m.minGroupPeople > 0 AND IFNULL(sc.signupCount, 0) >= m.minGroupPeople THEN 'formed'
|
|
||||||
ELSE 'unformed'
|
|
||||||
END AS groupStatus,
|
|
||||||
CASE
|
|
||||||
WHEN m.maxGroupPeople IS NOT NULL AND m.maxGroupPeople > 0 AND IFNULL(sc.signupCount, 0) > m.maxGroupPeople THEN '超员'
|
|
||||||
WHEN m.minGroupPeople IS NOT NULL AND m.minGroupPeople > 0 AND IFNULL(sc.signupCount, 0) >= m.minGroupPeople THEN '已成团'
|
|
||||||
ELSE '未成团'
|
|
||||||
END AS groupStatusName
|
|
||||||
FROM tour_matter m
|
FROM tour_matter m
|
||||||
INNER JOIN tour_line l ON l.id = m.lineId
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
LEFT JOIN sys_union u ON u.id = m.unionId
|
INNER JOIN (
|
||||||
LEFT JOIN (
|
SELECT
|
||||||
SELECT t.matterId, SUM(1 + IFNULL(f.familyCount, 0)) AS signupCount
|
t.matterId,
|
||||||
|
t.groupCode,
|
||||||
|
MAX(t.groupName) AS groupName,
|
||||||
|
MAX(t.undertakeUnionId) AS undertakeUnionId,
|
||||||
|
MAX(t.undertakeUnionName) AS undertakeUnionName,
|
||||||
|
SUM(1 + IFNULL(f.familyCount, 0)) AS signupCount
|
||||||
FROM tour_ledger t
|
FROM tour_ledger t
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ledgerId, COUNT(1) AS familyCount
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
@@ -122,12 +133,16 @@ public class TourUndertakeController {
|
|||||||
GROUP BY ledgerId
|
GROUP BY ledgerId
|
||||||
) f ON f.ledgerId = t.id
|
) f ON f.ledgerId = t.id
|
||||||
WHERE t.delFlag = 0
|
WHERE t.delFlag = 0
|
||||||
AND t.matterId IS NOT NULL
|
AND t.groupCode IS NOT NULL
|
||||||
AND t.matterId <> ''
|
AND t.groupCode <> ''
|
||||||
GROUP BY t.matterId
|
GROUP BY t.matterId, t.groupCode
|
||||||
) sc ON sc.matterId = m.id
|
) gs ON gs.matterId = m.id
|
||||||
|
LEFT JOIN sys_union uu ON uu.id = gs.undertakeUnionId
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT t.matterId, SUM(1 + IFNULL(f.familyCount, 0)) AS currentUnionSignupCount
|
SELECT
|
||||||
|
t.matterId,
|
||||||
|
t.groupCode,
|
||||||
|
SUM(1 + IFNULL(f.familyCount, 0)) AS currentUnionSignupCount
|
||||||
FROM tour_ledger t
|
FROM tour_ledger t
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ledgerId, COUNT(1) AS familyCount
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
@@ -136,13 +151,13 @@ public class TourUndertakeController {
|
|||||||
GROUP BY ledgerId
|
GROUP BY ledgerId
|
||||||
) f ON f.ledgerId = t.id
|
) f ON f.ledgerId = t.id
|
||||||
WHERE t.delFlag = 0
|
WHERE t.delFlag = 0
|
||||||
AND t.matterId IS NOT NULL
|
AND t.groupCode IS NOT NULL
|
||||||
AND t.matterId <> ''
|
AND t.groupCode <> ''
|
||||||
AND t.unionId = @currentUnionId
|
AND t.unionId = @currentUnionId
|
||||||
GROUP BY t.matterId
|
GROUP BY t.matterId, t.groupCode
|
||||||
) usc ON usc.matterId = m.id
|
) ugs ON ugs.matterId = m.id AND ugs.groupCode = gs.groupCode
|
||||||
$condition
|
$condition
|
||||||
ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC
|
ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, gs.groupCode ASC, m.createdAt DESC
|
||||||
""");
|
""");
|
||||||
listSql.setCondition(cnd);
|
listSql.setCondition(cnd);
|
||||||
listSql.setParam("currentUnionId", currentUnionId);
|
listSql.setParam("currentUnionId", currentUnionId);
|
||||||
@@ -180,49 +195,35 @@ public class TourUndertakeController {
|
|||||||
return Result.success(unionInfo);
|
return Result.success(unionInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领办当前成团行,接口必须同时接收事项ID和成团编码,避免按事项误领办全部团。
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("tour.undertake")
|
@SaCheckPermission("tour.undertake")
|
||||||
public Result undertakeLine(String matterId) {
|
public Result undertakeLine(String matterId, String groupCode) {
|
||||||
if (StrUtil.isBlank(matterId)) {
|
if (StrUtil.isBlank(matterId) || StrUtil.isBlank(groupCode)) {
|
||||||
return Result.error("参数错误");
|
return Result.error("参数错误");
|
||||||
}
|
}
|
||||||
NutMap unionInfo = currentUnionInfo();
|
NutMap unionInfo = currentUnionInfo();
|
||||||
if (unionInfo.isEmpty()) {
|
if (unionInfo.isEmpty()) {
|
||||||
return Result.error("未获取到当前登录人工会");
|
return Result.error("未获取到当前登录人工会");
|
||||||
}
|
}
|
||||||
TourMatter matter = tourMatterService.fetch(matterId);
|
return tourLedgerService.undertakeGroup(matterId, groupCode, unionInfo.getString("unionId"), unionInfo.getString("unionName"));
|
||||||
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag())) {
|
|
||||||
return Result.error("事项不存在");
|
|
||||||
}
|
|
||||||
if ("claimed".equals(matter.getUndertakeStatus())) {
|
|
||||||
return Result.error("当前线路已领办,不允许修改");
|
|
||||||
}
|
|
||||||
matter.setUnionId(unionInfo.getString("unionId"));
|
|
||||||
matter.setUndertakeStatus("claimed");
|
|
||||||
tourMatterService.update(matter);
|
|
||||||
return Result.success(unionInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解除当前成团行的领办信息,清空范围只限定到当前事项的当前团。
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("tour.undertake")
|
@SaCheckPermission("tour.undertake")
|
||||||
public Result releaseUndertakeLine(String matterId) {
|
public Result releaseUndertakeLine(String matterId, String groupCode) {
|
||||||
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) {
|
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) {
|
||||||
return Result.error("仅系统管理员可以解除领办");
|
return Result.error("仅系统管理员可以解除领办");
|
||||||
}
|
}
|
||||||
if (StrUtil.isBlank(matterId)) {
|
if (StrUtil.isBlank(matterId) || StrUtil.isBlank(groupCode)) {
|
||||||
return Result.error("参数错误");
|
return Result.error("参数错误");
|
||||||
}
|
}
|
||||||
TourMatter matter = tourMatterService.fetch(matterId);
|
return tourLedgerService.releaseUndertakeGroup(matterId, groupCode);
|
||||||
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag())) {
|
|
||||||
return Result.error("事项不存在");
|
|
||||||
}
|
|
||||||
if (!"claimed".equals(matter.getUndertakeStatus())) {
|
|
||||||
return Result.error("当前线路未领办,无需解除");
|
|
||||||
}
|
|
||||||
matter.setUnionId("");
|
|
||||||
matter.setUndertakeStatus("unclaimed");
|
|
||||||
tourMatterService.update(matter);
|
|
||||||
return Result.success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@@ -259,20 +260,30 @@ public class TourUndertakeController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("tour.undertake")
|
@SaCheckPermission("tour.undertake")
|
||||||
public Result signupPageData(PageForm pageForm, String matterId, String keyword, String unionId) {
|
public Result signupPageData(PageForm pageForm, String matterId, String keyword, String unionId, String groupCode) {
|
||||||
if (StrUtil.isBlank(matterId)) {
|
if (StrUtil.isBlank(matterId) || StrUtil.isBlank(groupCode)) {
|
||||||
return Result.error("参数错误");
|
return Result.error("参数错误");
|
||||||
}
|
}
|
||||||
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
||||||
pageForm.defaultSortDesc("signupTime");
|
pageForm.defaultSortAsc("signupTime");
|
||||||
}
|
}
|
||||||
Cnd cnd = buildSignupQueryCnd(matterId, keyword, unionId);
|
Cnd cnd = buildSignupQueryCnd(matterId, keyword, unionId, groupCode);
|
||||||
Sql countSql = Sqls.create("""
|
Sql countSql = Sqls.create("""
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
|
FROM (
|
||||||
|
SELECT t.id AS rowId
|
||||||
FROM tour_ledger t
|
FROM tour_ledger t
|
||||||
INNER JOIN tour_matter m ON m.id = t.matterId
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
INNER JOIN tour_line l ON l.id = m.lineId
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
$condition
|
$condition
|
||||||
|
UNION ALL
|
||||||
|
SELECT f.id AS rowId
|
||||||
|
FROM tour_ledger t
|
||||||
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
INNER JOIN tour_ledger_family f ON f.ledgerId = t.id AND f.delFlag = 0
|
||||||
|
$condition
|
||||||
|
) p
|
||||||
""");
|
""");
|
||||||
countSql.setCondition(cnd);
|
countSql.setCondition(cnd);
|
||||||
countSql.setCallback(Sqls.callback.integer());
|
countSql.setCallback(Sqls.callback.integer());
|
||||||
@@ -280,23 +291,63 @@ public class TourUndertakeController {
|
|||||||
int count = countSql.getInt();
|
int count = countSql.getInt();
|
||||||
|
|
||||||
Sql listSql = Sqls.create("""
|
Sql listSql = Sqls.create("""
|
||||||
|
SELECT *
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
|
t.id AS ledgerId,
|
||||||
t.jobNo,
|
t.jobNo,
|
||||||
t.userName,
|
t.userName,
|
||||||
|
t.gender,
|
||||||
'' AS mobile,
|
'' AS mobile,
|
||||||
t.idCard,
|
t.idCard,
|
||||||
t.unionId,
|
t.unionId,
|
||||||
t.unionName,
|
t.unionName,
|
||||||
t.signupTime
|
t.signupTime,
|
||||||
|
t.groupCode,
|
||||||
|
t.groupName,
|
||||||
|
1 AS staffRow,
|
||||||
|
'教职工' AS rowTypeName,
|
||||||
|
'' AS relationship,
|
||||||
|
t.userName AS relatedStaffName,
|
||||||
|
0 AS rowSort,
|
||||||
|
t.createdAt AS ledgerCreatedAt,
|
||||||
|
t.createdAt
|
||||||
FROM tour_ledger t
|
FROM tour_ledger t
|
||||||
INNER JOIN tour_matter m ON m.id = t.matterId
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
INNER JOIN tour_line l ON l.id = m.lineId
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
$condition
|
$condition
|
||||||
ORDER BY $orderColumn $orderBy, t.signupTime DESC, t.createdAt DESC
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
f.id,
|
||||||
|
t.id AS ledgerId,
|
||||||
|
'' AS jobNo,
|
||||||
|
f.familyName AS userName,
|
||||||
|
f.gender,
|
||||||
|
'' AS mobile,
|
||||||
|
f.idCard,
|
||||||
|
t.unionId,
|
||||||
|
'' AS unionName,
|
||||||
|
t.signupTime,
|
||||||
|
t.groupCode,
|
||||||
|
t.groupName,
|
||||||
|
0 AS staffRow,
|
||||||
|
'家属' AS rowTypeName,
|
||||||
|
f.relationship,
|
||||||
|
t.userName AS relatedStaffName,
|
||||||
|
1 AS rowSort,
|
||||||
|
t.createdAt AS ledgerCreatedAt,
|
||||||
|
f.createdAt
|
||||||
|
FROM tour_ledger t
|
||||||
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
INNER JOIN tour_ledger_family f ON f.ledgerId = t.id AND f.delFlag = 0
|
||||||
|
$condition
|
||||||
|
) p
|
||||||
|
ORDER BY $orderColumn $orderBy, p.signupTime ASC, p.ledgerCreatedAt ASC, p.rowSort ASC, p.createdAt ASC
|
||||||
""");
|
""");
|
||||||
listSql.setCondition(cnd);
|
listSql.setCondition(cnd);
|
||||||
listSql.setVar("orderColumn", getSignupOrderColumn(pageForm.getPageOrderName()));
|
listSql.setVar("orderColumn", getSignupUnionOrderColumn(pageForm.getPageOrderName()));
|
||||||
listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc");
|
listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc");
|
||||||
listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||||
listSql.setCallback(Sqls.callback.maps());
|
listSql.setCallback(Sqls.callback.maps());
|
||||||
@@ -304,15 +355,138 @@ public class TourUndertakeController {
|
|||||||
|
|
||||||
List<NutMap> list = listSql.getList(NutMap.class);
|
List<NutMap> list = listSql.getList(NutMap.class);
|
||||||
fillSignupMobile(list);
|
fillSignupMobile(list);
|
||||||
fillSignupFamilies(list);
|
|
||||||
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
|
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
|
||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cnd buildSignupQueryCnd(String matterId, String keyword, String unionId) {
|
/**
|
||||||
|
* 导出领办查看弹窗中的报名人员数据,导出范围复用查看弹窗的事项、成团、关键字和工会条件。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("tour.undertake")
|
||||||
|
@Ok("void")
|
||||||
|
public void exportSignupData(PageForm pageForm, String matterId, String keyword, String unionId, String groupCode,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
|
||||||
|
pageForm.defaultSortAsc("signupTime");
|
||||||
|
}
|
||||||
|
// 导出不分页,保持筛选、排序、手机号补充和家属平铺逻辑与查看弹窗一致。
|
||||||
|
List<NutMap> rows = querySignupExportRows(pageForm, matterId, keyword, unionId, groupCode);
|
||||||
|
List<ExcelExportEntity> entities = buildSignupExportEntities();
|
||||||
|
|
||||||
|
ExportParams exportParams = new ExportParams();
|
||||||
|
exportParams.setType(ExcelType.XSSF);
|
||||||
|
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, rows);
|
||||||
|
CommonDownloadUtil.download("领办报名人员.xlsx", workbook, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询领办查看弹窗导出数据,复用查看列表的权限范围和人员组装逻辑,仅去掉分页限制。
|
||||||
|
*/
|
||||||
|
private List<NutMap> querySignupExportRows(PageForm pageForm, String matterId, String keyword, String unionId, String groupCode) {
|
||||||
|
Cnd cnd = buildSignupQueryCnd(matterId, keyword, unionId, groupCode);
|
||||||
|
Sql listSql = Sqls.create("""
|
||||||
|
SELECT *
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.id AS ledgerId,
|
||||||
|
t.jobNo,
|
||||||
|
t.userName,
|
||||||
|
t.gender,
|
||||||
|
'' AS mobile,
|
||||||
|
t.idCard,
|
||||||
|
t.unionId,
|
||||||
|
t.unionName,
|
||||||
|
t.signupTime,
|
||||||
|
t.groupCode,
|
||||||
|
t.groupName,
|
||||||
|
1 AS staffRow,
|
||||||
|
'教职工' AS rowTypeName,
|
||||||
|
'' AS relationship,
|
||||||
|
t.userName AS relatedStaffName,
|
||||||
|
0 AS rowSort,
|
||||||
|
t.createdAt AS ledgerCreatedAt,
|
||||||
|
t.createdAt
|
||||||
|
FROM tour_ledger t
|
||||||
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
$condition
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
f.id,
|
||||||
|
t.id AS ledgerId,
|
||||||
|
'' AS jobNo,
|
||||||
|
f.familyName AS userName,
|
||||||
|
f.gender,
|
||||||
|
'' AS mobile,
|
||||||
|
f.idCard,
|
||||||
|
t.unionId,
|
||||||
|
'' AS unionName,
|
||||||
|
t.signupTime,
|
||||||
|
t.groupCode,
|
||||||
|
t.groupName,
|
||||||
|
0 AS staffRow,
|
||||||
|
'家属' AS rowTypeName,
|
||||||
|
f.relationship,
|
||||||
|
t.userName AS relatedStaffName,
|
||||||
|
1 AS rowSort,
|
||||||
|
t.createdAt AS ledgerCreatedAt,
|
||||||
|
f.createdAt
|
||||||
|
FROM tour_ledger t
|
||||||
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
INNER JOIN tour_ledger_family f ON f.ledgerId = t.id AND f.delFlag = 0
|
||||||
|
$condition
|
||||||
|
) p
|
||||||
|
ORDER BY $orderColumn $orderBy, p.signupTime ASC, p.ledgerCreatedAt ASC, p.rowSort ASC, p.createdAt ASC
|
||||||
|
""");
|
||||||
|
listSql.setCondition(cnd);
|
||||||
|
listSql.setVar("orderColumn", getSignupUnionOrderColumn(pageForm.getPageOrderName()));
|
||||||
|
listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc");
|
||||||
|
listSql.setCallback(Sqls.callback.maps());
|
||||||
|
tourLedgerService.dao().execute(listSql);
|
||||||
|
|
||||||
|
List<NutMap> list = listSql.getList(NutMap.class);
|
||||||
|
fillSignupMobile(list);
|
||||||
|
// 补齐导出专用序号和成团展示字段,确保 Excel 列显示与查看弹窗一致。
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
NutMap item = list.get(i);
|
||||||
|
item.put("indexNo", i + 1);
|
||||||
|
String groupCodeText = item.getString("groupCode", "");
|
||||||
|
String groupNameText = item.getString("groupName", "");
|
||||||
|
item.put("groupInfo", StrUtil.isBlank(groupCodeText) ? "未成团" : groupCodeText + (StrUtil.isBlank(groupNameText) ? "" : " " + groupNameText));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义领办查看弹窗导出列,字段顺序与弹窗表格保持一致。
|
||||||
|
*/
|
||||||
|
private List<ExcelExportEntity> buildSignupExportEntities() {
|
||||||
|
List<ExcelExportEntity> entities = new ArrayList<>();
|
||||||
|
entities.add(new ExcelExportEntity("序号", "indexNo", 10));
|
||||||
|
entities.add(new ExcelExportEntity("工号", "jobNo", 14));
|
||||||
|
entities.add(new ExcelExportEntity("姓名", "userName", 14));
|
||||||
|
entities.add(new ExcelExportEntity("性别", "gender", 10));
|
||||||
|
entities.add(new ExcelExportEntity("手机号码", "mobile", 18));
|
||||||
|
entities.add(new ExcelExportEntity("身份证号", "idCard", 24));
|
||||||
|
entities.add(new ExcelExportEntity("所属工会", "unionName", 24));
|
||||||
|
entities.add(new ExcelExportEntity("人员类型", "rowTypeName", 12));
|
||||||
|
entities.add(new ExcelExportEntity("亲属关系", "relationship", 14));
|
||||||
|
entities.add(new ExcelExportEntity("成团信息", "groupInfo", 24));
|
||||||
|
entities.add(new ExcelExportEntity("关联教职工名称", "relatedStaffName", 18));
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领办线路查看以团为单位,报名人员查询必须固定到当前点击的成团编码。
|
||||||
|
*/
|
||||||
|
private Cnd buildSignupQueryCnd(String matterId, String keyword, String unionId, String groupCode) {
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
cnd.and("t.delFlag", "=", false);
|
cnd.and("t.delFlag", "=", false);
|
||||||
cnd.and("t.matterId", "=", matterId);
|
cnd.and("t.matterId", "=", matterId);
|
||||||
|
cnd.and("t.groupCode", "=", groupCode);
|
||||||
cnd.and("m.delFlag", "=", false);
|
cnd.and("m.delFlag", "=", false);
|
||||||
cnd.and("m.enabled", "=", true);
|
cnd.and("m.enabled", "=", true);
|
||||||
cnd.and("m.lineId", "IS NOT", null);
|
cnd.and("m.lineId", "IS NOT", null);
|
||||||
@@ -360,9 +534,9 @@ public class TourUndertakeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查看成团报名人员时按台账ID带出家属子列表,避免前端逐行请求家属信息。
|
* 查看单个团报名人员时将教职工和家属平铺到同一列表,便于领办线路复用线路成团的展示方式。
|
||||||
*/
|
*/
|
||||||
private void fillSignupFamilies(List<NutMap> list) {
|
private void flattenSignupFamilies(List<NutMap> list) {
|
||||||
if (list == null || list.isEmpty()) {
|
if (list == null || list.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -380,8 +554,9 @@ public class TourUndertakeController {
|
|||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
ledgerId,
|
ledgerId,
|
||||||
staffJobNo AS jobNo,
|
'' AS jobNo,
|
||||||
familyName AS userName,
|
familyName AS userName,
|
||||||
|
gender,
|
||||||
'' AS mobile,
|
'' AS mobile,
|
||||||
idCard,
|
idCard,
|
||||||
relationship
|
relationship
|
||||||
@@ -396,11 +571,29 @@ public class TourUndertakeController {
|
|||||||
|
|
||||||
Map<String, List<NutMap>> familyMap = new HashMap<>();
|
Map<String, List<NutMap>> familyMap = new HashMap<>();
|
||||||
for (NutMap family : sql.getList(NutMap.class)) {
|
for (NutMap family : sql.getList(NutMap.class)) {
|
||||||
|
family.put("staffRow", false);
|
||||||
|
family.put("rowTypeName", "家属");
|
||||||
|
family.put("unionName", "");
|
||||||
familyMap.computeIfAbsent(family.getString("ledgerId", ""), key -> new ArrayList<>()).add(family);
|
familyMap.computeIfAbsent(family.getString("ledgerId", ""), key -> new ArrayList<>()).add(family);
|
||||||
}
|
}
|
||||||
|
List<NutMap> flatList = new ArrayList<>();
|
||||||
for (NutMap item : list) {
|
for (NutMap item : list) {
|
||||||
item.put("families", familyMap.getOrDefault(item.getString("id", ""), List.of()));
|
item.put("staffRow", true);
|
||||||
|
item.put("rowTypeName", "教职工");
|
||||||
|
item.put("relationship", "");
|
||||||
|
item.put("relatedStaffName", item.getString("userName", ""));
|
||||||
|
flatList.add(item);
|
||||||
|
List<NutMap> families = familyMap.getOrDefault(item.getString("id", ""), List.of());
|
||||||
|
for (NutMap family : families) {
|
||||||
|
// 家属行继承教职工成团信息,并标明关联教职工名称。
|
||||||
|
family.put("relatedStaffName", item.getString("userName", ""));
|
||||||
|
family.put("groupCode", item.getString("groupCode", ""));
|
||||||
|
family.put("groupName", item.getString("groupName", ""));
|
||||||
}
|
}
|
||||||
|
flatList.addAll(families);
|
||||||
|
}
|
||||||
|
list.clear();
|
||||||
|
list.addAll(flatList);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId, String undertakeStatus) {
|
private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId, String undertakeStatus) {
|
||||||
@@ -414,14 +607,14 @@ public class TourUndertakeController {
|
|||||||
cnd.andEX("m.`year`", "=", year == null ? LocalDate.now().getYear() : year);
|
cnd.andEX("m.`year`", "=", year == null ? LocalDate.now().getYear() : year);
|
||||||
cnd.and(Cnd.likeEX("l.lineName", lineName));
|
cnd.and(Cnd.likeEX("l.lineName", lineName));
|
||||||
cnd.andEX("l.lineType", "=", lineType);
|
cnd.andEX("l.lineType", "=", lineType);
|
||||||
cnd.andEX("m.unionId", "=", unionId);
|
cnd.andEX("gs.undertakeUnionId", "=", unionId);
|
||||||
if ("claimed".equals(undertakeStatus)) {
|
if ("claimed".equals(undertakeStatus)) {
|
||||||
cnd.and("m.undertakeStatus", "=", "claimed");
|
cnd.and("gs.undertakeUnionId", "IS NOT", null);
|
||||||
|
cnd.and("gs.undertakeUnionId", "<>", "");
|
||||||
} else if ("unclaimed".equals(undertakeStatus)) {
|
} else if ("unclaimed".equals(undertakeStatus)) {
|
||||||
SqlExpressionGroup statusGroup = new SqlExpressionGroup();
|
SqlExpressionGroup statusGroup = new SqlExpressionGroup();
|
||||||
statusGroup.or("m.undertakeStatus", "=", "unclaimed");
|
statusGroup.or("gs.undertakeUnionId", "IS", null);
|
||||||
statusGroup.or("m.undertakeStatus", "IS", null);
|
statusGroup.or("gs.undertakeUnionId", "=", "");
|
||||||
statusGroup.or("m.undertakeStatus", "=", "");
|
|
||||||
cnd.and(statusGroup);
|
cnd.and(statusGroup);
|
||||||
}
|
}
|
||||||
applyMatterDataScope(cnd);
|
applyMatterDataScope(cnd);
|
||||||
@@ -432,7 +625,9 @@ public class TourUndertakeController {
|
|||||||
* 线路成团按事项归属控制数据范围:校工会查看全部;分工会查看本工会事项;其它角色仅查看自己创建的事项。
|
* 线路成团按事项归属控制数据范围:校工会查看全部;分工会查看本工会事项;其它角色仅查看自己创建的事项。
|
||||||
*/
|
*/
|
||||||
private void applyMatterDataScope(Cnd cnd) {
|
private void applyMatterDataScope(Cnd cnd) {
|
||||||
if (hasSchoolUnionScope() || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
|
if (hasSchoolUnionScope()
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|
||||||
|
|| hasTourApplicantScope()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hasBranchUnionScope()) {
|
if (hasBranchUnionScope()) {
|
||||||
@@ -460,6 +655,13 @@ public class TourUndertakeController {
|
|||||||
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 疗休养领办人只扩大领办线路查询范围,不改变领办、解除等操作权限。
|
||||||
|
*/
|
||||||
|
private boolean hasTourApplicantScope() {
|
||||||
|
return AuthUtil.hasRole(RoleConstant.TOUR_APPLICANT.name());
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasBranchUnionScope() {
|
private boolean hasBranchUnionScope() {
|
||||||
return AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name())
|
return AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name())
|
||||||
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|
||||||
@@ -489,7 +691,7 @@ public class TourUndertakeController {
|
|||||||
return "m.matterName";
|
return "m.matterName";
|
||||||
}
|
}
|
||||||
if ("unionName".equals(orderName)) {
|
if ("unionName".equals(orderName)) {
|
||||||
return "u.name";
|
return "unionName";
|
||||||
}
|
}
|
||||||
if ("undertakeStatusName".equals(orderName)) {
|
if ("undertakeStatusName".equals(orderName)) {
|
||||||
return "undertakeStatus";
|
return "undertakeStatus";
|
||||||
@@ -498,33 +700,33 @@ public class TourUndertakeController {
|
|||||||
return "l.lineType";
|
return "l.lineType";
|
||||||
}
|
}
|
||||||
if ("signupCount".equals(orderName)) {
|
if ("signupCount".equals(orderName)) {
|
||||||
return "signupCount";
|
return "gs.signupCount";
|
||||||
}
|
}
|
||||||
if ("minGroupPeople".equals(orderName)) {
|
if ("groupName".equals(orderName) || "groupCode".equals(orderName)) {
|
||||||
return "m.minGroupPeople";
|
return "gs.groupCode";
|
||||||
}
|
|
||||||
if ("maxGroupPeople".equals(orderName)) {
|
|
||||||
return "m.maxGroupPeople";
|
|
||||||
}
|
|
||||||
if ("groupStatusName".equals(orderName)) {
|
|
||||||
return "groupStatus";
|
|
||||||
}
|
}
|
||||||
return "l.lineName";
|
return "l.lineName";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getSignupOrderColumn(String orderName) {
|
private String getSignupUnionOrderColumn(String orderName) {
|
||||||
if ("jobNo".equals(orderName)) {
|
if ("jobNo".equals(orderName)) {
|
||||||
return "t.jobNo";
|
return "p.jobNo";
|
||||||
}
|
}
|
||||||
if ("userName".equals(orderName)) {
|
if ("userName".equals(orderName)) {
|
||||||
return "t.userName";
|
return "p.userName";
|
||||||
|
}
|
||||||
|
if ("gender".equals(orderName)) {
|
||||||
|
return "p.gender";
|
||||||
}
|
}
|
||||||
if ("idCard".equals(orderName)) {
|
if ("idCard".equals(orderName)) {
|
||||||
return "t.idCard";
|
return "p.idCard";
|
||||||
}
|
}
|
||||||
if ("unionName".equals(orderName)) {
|
if ("unionName".equals(orderName)) {
|
||||||
return "t.unionName";
|
return "p.unionName";
|
||||||
}
|
}
|
||||||
return "t.signupTime";
|
if ("groupCode".equals(orderName) || "groupName".equals(orderName)) {
|
||||||
|
return "p.groupCode";
|
||||||
|
}
|
||||||
|
return "p.signupTime";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-24
@@ -97,18 +97,7 @@ public class TourUnionApprovalController {
|
|||||||
task.taskParentId,
|
task.taskParentId,
|
||||||
task.variable AS taskVariable,
|
task.variable AS taskVariable,
|
||||||
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName,
|
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName,
|
||||||
CASE
|
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName,
|
||||||
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
|
|
||||||
ELSE CONCAT(
|
|
||||||
COALESCE(NULLIF(l.lineName, ''), t.lineName),
|
|
||||||
'(',
|
|
||||||
CASE
|
|
||||||
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
|
|
||||||
ELSE IFNULL(mu.name, '')
|
|
||||||
END,
|
|
||||||
')'
|
|
||||||
)
|
|
||||||
END AS currentLineName,
|
|
||||||
CASE
|
CASE
|
||||||
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
||||||
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
||||||
@@ -479,18 +468,7 @@ public class TourUnionApprovalController {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT CASE
|
SELECT COALESCE(NULLIF(l.lineName, ''), t.lineName) AS lineName
|
||||||
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
|
|
||||||
ELSE CONCAT(
|
|
||||||
COALESCE(NULLIF(l.lineName, ''), t.lineName),
|
|
||||||
'(',
|
|
||||||
CASE
|
|
||||||
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
|
|
||||||
ELSE IFNULL(mu.name, '')
|
|
||||||
END,
|
|
||||||
')'
|
|
||||||
)
|
|
||||||
END AS lineName
|
|
||||||
FROM tour_ledger t
|
FROM tour_ledger t
|
||||||
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
||||||
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
|
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
|
||||||
|
|||||||
+2
-24
@@ -94,18 +94,7 @@ public class TourUnionLedgerController {
|
|||||||
ins.processDefineId AS instanceProcessDefineId,
|
ins.processDefineId AS instanceProcessDefineId,
|
||||||
IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine,
|
IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine,
|
||||||
IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName,
|
IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName,
|
||||||
CASE
|
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName,
|
||||||
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
|
|
||||||
ELSE CONCAT(
|
|
||||||
COALESCE(NULLIF(l.lineName, ''), t.lineName),
|
|
||||||
'(',
|
|
||||||
CASE
|
|
||||||
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
|
|
||||||
ELSE IFNULL(mu.name, '')
|
|
||||||
END,
|
|
||||||
')'
|
|
||||||
)
|
|
||||||
END AS currentLineName,
|
|
||||||
IFNULL(f.familyCount, 0) AS familyCount,
|
IFNULL(f.familyCount, 0) AS familyCount,
|
||||||
CASE
|
CASE
|
||||||
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
||||||
@@ -804,18 +793,7 @@ public class TourUnionLedgerController {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT CASE
|
SELECT COALESCE(NULLIF(l.lineName, ''), t.lineName) AS lineName
|
||||||
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
|
|
||||||
ELSE CONCAT(
|
|
||||||
COALESCE(NULLIF(l.lineName, ''), t.lineName),
|
|
||||||
'(',
|
|
||||||
CASE
|
|
||||||
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
|
|
||||||
ELSE IFNULL(mu.name, '')
|
|
||||||
END,
|
|
||||||
')'
|
|
||||||
)
|
|
||||||
END AS lineName
|
|
||||||
FROM tour_ledger t
|
FROM tour_ledger t
|
||||||
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
||||||
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
|
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
|
||||||
|
|||||||
@@ -101,6 +101,32 @@ public class TourLedger extends BaseModel implements Serializable {
|
|||||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
private String lineType;
|
private String lineType;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("成团编码")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
|
private String groupCode;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("成团名称")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
|
private String groupName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领办工会编码按团回填到台账,避免领办动作再修改事项归属工会。
|
||||||
|
*/
|
||||||
|
@Column
|
||||||
|
@Comment("领办工会编码")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
private String undertakeUnionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领办工会名称与编码一起落台账,列表按团展示时直接读取当前团领办信息。
|
||||||
|
*/
|
||||||
|
@Column
|
||||||
|
@Comment("领办工会")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String undertakeUnionName;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("报名酒店")
|
@Comment("报名酒店")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ public class TourLine extends BaseModel implements Serializable {
|
|||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("排序编号")
|
@Comment("排序编号")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
@ColDefine(type = ColType.INT, width = 11)
|
||||||
private String lineCode;
|
private Integer lineCode;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("线路名称")
|
@Comment("线路名称")
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ public class TourMatter extends BaseModel implements Serializable {
|
|||||||
@ColDefine(type = ColType.INT)
|
@ColDefine(type = ColType.INT)
|
||||||
private Integer maxGroupPeople;
|
private Integer maxGroupPeople;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("最多报名人数")
|
||||||
|
@ColDefine(type = ColType.INT)
|
||||||
|
private Integer maxSignupPeople;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("预计费用")
|
@Comment("预计费用")
|
||||||
@ColDefine(customType = "decimal(10,2)")
|
@ColDefine(customType = "decimal(10,2)")
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.tour.models;
|
||||||
|
|
||||||
|
import com.budwk.app.base.model.BaseModel;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
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.Default;
|
||||||
|
import org.nutz.dao.entity.annotation.Name;
|
||||||
|
import org.nutz.dao.entity.annotation.Table;
|
||||||
|
import org.nutz.dao.entity.annotation.TableMeta;
|
||||||
|
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tour participant roster stores every eligible staff member for a tour setting.
|
||||||
|
* It is used for joined/not-joined statistics and links to tour_ledger only when the user has a signup record.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Table("tour_participant_roster")
|
||||||
|
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
||||||
|
@Comment("疗休养人员名单")
|
||||||
|
public class TourParticipantRoster extends BaseModel implements Serializable {
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@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 settingId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("工号")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
|
private String jobNo;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("姓名")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("所在单位ID")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
private String unitId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("所在单位")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String unitName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("所在工会ID")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
private String unionId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("所在工会")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String unionName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("是否参加")
|
||||||
|
@ColDefine(type = ColType.BOOLEAN)
|
||||||
|
@Default("0")
|
||||||
|
private Boolean joined;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("疗休养台账ID")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
private String ledgerId;
|
||||||
|
}
|
||||||
@@ -1,7 +1,101 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.tour.service;
|
package com.budwk.app.zhgh.dayofficework.tour.service;
|
||||||
|
|
||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface TourLedgerService extends BaseService<TourLedger> {
|
public interface TourLedgerService extends BaseService<TourLedger> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按查询条件获取疗休养台账中已分配的成团编码和成团名称,返回值已按成团信息去重。
|
||||||
|
*
|
||||||
|
* @param cnd 已包含事项、线路、台账状态和数据权限的查询条件
|
||||||
|
* @return 去重后的成团信息列表
|
||||||
|
*/
|
||||||
|
List<NutMap> queryGroupOptions(Cnd cnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按疗休养台账页面的完整查询条件获取导出数据,不进行分页。
|
||||||
|
*
|
||||||
|
* @param cnd 台账查询条件,包含年度范围、姓名或工号、所属工会、线路、出行时段、线路类型、
|
||||||
|
* 直系亲属单位线路人员、超出费用由单位承担人员及审批完成状态限制
|
||||||
|
* @return 台账导出数据列表;每项包含年度、人员、线路、成团、领办工会、报名、家属及参加状态信息
|
||||||
|
*/
|
||||||
|
List<NutMap> queryExportRows(Cnd cnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按事项最大成团人数,将当前事项下未成团的报名教职工及家属整体自动分配到成团中。
|
||||||
|
*
|
||||||
|
* @param matterId 疗休养事项ID
|
||||||
|
* @return 分配结果,包含成功提示或具体业务校验错误
|
||||||
|
*/
|
||||||
|
Result assignGroup(String matterId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将选中的教职工报名台账调整到指定成团中,并校验调整后目标团人数不超过事项最大成团人数。
|
||||||
|
*
|
||||||
|
* @param matterId 当前疗休养事项ID
|
||||||
|
* @param groupCode 目标成团编码
|
||||||
|
* @param ledgerIds 选中的教职工台账ID,多个ID使用逗号分隔
|
||||||
|
* @return 调整结果,包含成功提示或具体业务校验错误
|
||||||
|
*/
|
||||||
|
Result adjustGroup(String matterId, String groupCode, String ledgerIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按事项和成团编码领办当前团,只回填当前团台账的领办工会信息,不修改事项表归属工会。
|
||||||
|
*
|
||||||
|
* @param matterId 当前疗休养事项ID
|
||||||
|
* @param groupCode 当前成团编码
|
||||||
|
* @param unionId 当前登录人所属工会编码
|
||||||
|
* @param unionName 当前登录人所属工会名称
|
||||||
|
* @return 领办结果,包含参数、重复领办和有效团数据校验
|
||||||
|
*/
|
||||||
|
Result undertakeGroup(String matterId, String groupCode, String unionId, String unionName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按事项和成团编码解除当前团领办,只清空当前团台账的领办工会信息。
|
||||||
|
*
|
||||||
|
* @param matterId 当前疗休养事项ID
|
||||||
|
* @param groupCode 当前成团编码
|
||||||
|
* @return 解除结果,包含参数和当前团是否已领办校验
|
||||||
|
*/
|
||||||
|
Result releaseUndertakeGroup(String matterId, String groupCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks the active yearly tour setting row before out-province quota calculation.
|
||||||
|
*
|
||||||
|
* @param year tour signup year that shares one out-province quota pool.
|
||||||
|
*/
|
||||||
|
void lockTourSignupQuotaYear(Integer year);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks the current tour matter row before matter-level signup capacity calculation.
|
||||||
|
*
|
||||||
|
* @param matterId tour matter id that owns the maximum signup capacity limit.
|
||||||
|
*/
|
||||||
|
void lockTourSignupMatter(String matterId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the current time is inside the effective tour signup window.
|
||||||
|
* Setting-level signup time keeps the existing priority and falls back to matter-level time when blank.
|
||||||
|
*
|
||||||
|
* @param matter selected tour matter that carries the fallback signup window.
|
||||||
|
* @param setting tour setting that carries the primary signup window.
|
||||||
|
* @return null when current time is allowed; otherwise a business error result.
|
||||||
|
*/
|
||||||
|
Result checkSignupTimeWindow(TourMatter matter, TourSetting setting);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the tour signup page order column and keeps frontend sorting within known list fields.
|
||||||
|
*
|
||||||
|
* @param orderName requested frontend sort property.
|
||||||
|
* @return safe SQL order column for the tour signup list.
|
||||||
|
*/
|
||||||
|
String getTourSignupOrderColumn(String orderName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,17 @@ package com.budwk.app.zhgh.dayofficework.tour.service;
|
|||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface TourMatterService extends BaseService<TourMatter> {
|
public interface TourMatterService extends BaseService<TourMatter> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按事项ID批量更新报名起止时间,仅处理报名时间字段。
|
||||||
|
*
|
||||||
|
* @param ids 当前查询条件命中的事项ID集合
|
||||||
|
* @param signupStartTime 报名开始时间
|
||||||
|
* @param signupEndTime 报名结束时间
|
||||||
|
* @return 实际更新数量
|
||||||
|
*/
|
||||||
|
int assignSignupTime(List<String> ids, String signupStartTime, String signupEndTime);
|
||||||
}
|
}
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.tour.service;
|
||||||
|
|
||||||
|
import com.budwk.app.base.service.BaseService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourParticipantRoster;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 疗休养人员名单维护服务。
|
||||||
|
* 用于按配置维护可参与人员基础名单,并同步报名台账中的参加状态。
|
||||||
|
*/
|
||||||
|
public interface TourParticipantRosterService extends BaseService<TourParticipantRoster> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将一个疗休养配置的可参与人员同步到人员名单表。
|
||||||
|
*
|
||||||
|
* @param settingId 疗休养配置 id,配置中的可参与人员组别决定人员范围。
|
||||||
|
*/
|
||||||
|
void syncSettingRoster(String settingId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅当配置下还没有人员名单数据时,同步配置可参与人员。
|
||||||
|
*
|
||||||
|
* @param settingId 需要懒加载人员名单的疗休养配置 id。
|
||||||
|
*/
|
||||||
|
void syncSettingRosterIfEmpty(String settingId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据可参与人员范围和现有疗休养台账,重新生成所选配置的人员名单。
|
||||||
|
*
|
||||||
|
* @param settingIds 所选疗休养配置 id。
|
||||||
|
* @return 已不在可参与人员范围中但存在台账、因此无法剔除的人员提示。
|
||||||
|
*/
|
||||||
|
List<String> rebuildSettingRosters(List<String> settingIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除疗休养配置时清理对应人员名单。
|
||||||
|
*
|
||||||
|
* @param settingId 已删除的疗休养配置 id。
|
||||||
|
*/
|
||||||
|
void clearSettingRoster(String settingId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报名成功或修改报名后,将人员名单标记为已参加并关联最新台账。
|
||||||
|
*
|
||||||
|
* @param ledger 已创建或修改成功的报名台账。
|
||||||
|
*/
|
||||||
|
void markJoinedByLedger(TourLedger ledger);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消报名或删除报名后,将人员名单标记为未参加并清空关联台账。
|
||||||
|
*
|
||||||
|
* @param ledger 正在取消或删除的报名台账。
|
||||||
|
*/
|
||||||
|
void markNotJoinedByLedger(TourLedger ledger);
|
||||||
|
}
|
||||||
@@ -4,4 +4,11 @@ import com.budwk.app.base.service.BaseService;
|
|||||||
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
|
||||||
|
|
||||||
public interface TourSettingService extends BaseService<TourSetting> {
|
public interface TourSettingService extends BaseService<TourSetting> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新疗休养配置,并允许报名开始时间、报名结束时间被清空。
|
||||||
|
*
|
||||||
|
* @param tourSetting 页面提交的疗休养配置
|
||||||
|
*/
|
||||||
|
void updateWithNullableSignupTime(TourSetting tourSetting);
|
||||||
}
|
}
|
||||||
|
|||||||
+787
@@ -1,10 +1,32 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
|
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.dao.Chain;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.dao.Sqls;
|
||||||
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
public class TourLedgerServiceImpl extends BaseServiceImpl<TourLedger> implements TourLedgerService {
|
public class TourLedgerServiceImpl extends BaseServiceImpl<TourLedger> implements TourLedgerService {
|
||||||
@@ -12,4 +34,769 @@ public class TourLedgerServiceImpl extends BaseServiceImpl<TourLedger> implement
|
|||||||
public TourLedgerServiceImpl(Dao dao) {
|
public TourLedgerServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据传入条件查询台账中已经存在的成团信息,按成团编码和成团名称去重后返回给页面展示。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<NutMap> queryGroupOptions(Cnd cnd) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT DISTINCT
|
||||||
|
t.groupCode,
|
||||||
|
t.groupName
|
||||||
|
FROM tour_ledger t
|
||||||
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
$condition
|
||||||
|
ORDER BY t.groupCode ASC, t.groupName ASC
|
||||||
|
""");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
dao().execute(sql);
|
||||||
|
return sql.getList(NutMap.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询符合台账页面筛选条件的全部导出数据,关联线路、事项和家属数据以保持导出结果与列表一致。
|
||||||
|
*
|
||||||
|
* @param cnd 已包含页面全部筛选项和审批完成状态限制的台账查询条件
|
||||||
|
* @return 未分页的台账导出数据列表,每项为一名教职工的一条有效台账记录
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<NutMap> queryExportRows(Cnd cnd) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
t.`year`,
|
||||||
|
t.jobNo,
|
||||||
|
t.userName,
|
||||||
|
t.unitName,
|
||||||
|
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS lineName,
|
||||||
|
CASE
|
||||||
|
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
|
||||||
|
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
|
||||||
|
ELSE tp.travelPeriod
|
||||||
|
END AS travelPeriod,
|
||||||
|
t.lineType,
|
||||||
|
t.groupCode,
|
||||||
|
t.groupName,
|
||||||
|
t.undertakeUnionName,
|
||||||
|
t.signupTime,
|
||||||
|
IFNULL(f.familyCount, 0) AS familyCount,
|
||||||
|
t.joined,
|
||||||
|
t.overCostReimbursed
|
||||||
|
FROM tour_ledger t
|
||||||
|
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
|
FROM tour_ledger_family
|
||||||
|
WHERE delFlag = 0
|
||||||
|
GROUP BY ledgerId
|
||||||
|
) f ON f.ledgerId = t.id
|
||||||
|
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
||||||
|
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
|
||||||
|
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
`year`,
|
||||||
|
lineId,
|
||||||
|
GROUP_CONCAT(
|
||||||
|
DISTINCT CASE
|
||||||
|
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
|
||||||
|
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
ORDER BY travelStartTime ASC
|
||||||
|
SEPARATOR ';'
|
||||||
|
) AS travelPeriod
|
||||||
|
FROM tour_matter
|
||||||
|
WHERE delFlag = 0
|
||||||
|
AND lineId IS NOT NULL
|
||||||
|
AND lineId <> ''
|
||||||
|
GROUP BY `year`, lineId
|
||||||
|
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
|
||||||
|
$condition
|
||||||
|
GROUP BY t.id
|
||||||
|
ORDER BY t.`year` DESC, t.signupTime DESC, t.createdAt DESC
|
||||||
|
""");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
dao().execute(sql);
|
||||||
|
return sql.getList(NutMap.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动分配当前事项下未成团的教职工台账,分配过程中出现容量不合法时整体中止。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result assignGroup(String matterId) {
|
||||||
|
if (StrUtil.isBlank(matterId)) {
|
||||||
|
return Result.error("参数错误");
|
||||||
|
}
|
||||||
|
NutMap matter = fetchAssignMatter(matterId);
|
||||||
|
if (matter == null || matter.isEmpty()) {
|
||||||
|
return Result.error("事项不存在或线路信息不完整");
|
||||||
|
}
|
||||||
|
int maxGroupPeople = mapInt(matter, "maxGroupPeople");
|
||||||
|
if (maxGroupPeople <= 0) {
|
||||||
|
return Result.error("未配置最大成团人数,无法分配成团");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GroupBucket> groups = queryGroupBuckets(matterId);
|
||||||
|
List<SignupUnit> signupUnits = queryUnassignedSignupUnits(matterId);
|
||||||
|
if (signupUnits.isEmpty()) {
|
||||||
|
return Result.success("暂无未成团人员需要分配");
|
||||||
|
}
|
||||||
|
for (SignupUnit unit : signupUnits) {
|
||||||
|
if (unit.peopleCount > maxGroupPeople) {
|
||||||
|
return Result.error(unit.userName + "含家属共" + unit.peopleCount + "人,超过最大成团人数" + maxGroupPeople + "人,无法分配");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextGroupIndex = nextGroupIndex(groups);
|
||||||
|
int assignCount = 0;
|
||||||
|
for (SignupUnit unit : signupUnits) {
|
||||||
|
GroupBucket targetGroup = findAvailableGroup(groups, unit.peopleCount, maxGroupPeople);
|
||||||
|
if (targetGroup == null) {
|
||||||
|
targetGroup = createGroupBucket(nextGroupIndex++);
|
||||||
|
groups.add(targetGroup);
|
||||||
|
}
|
||||||
|
updateLedgerGroup(unit.ledgerId, targetGroup.groupCode, targetGroup.groupName);
|
||||||
|
targetGroup.peopleCount += unit.peopleCount;
|
||||||
|
assignCount++;
|
||||||
|
}
|
||||||
|
return Result.success("分配成功,共分配" + assignCount + "名教职工");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将选中的教职工台账调整到目标团,家属随教职工一起计入目标团人数。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result adjustGroup(String matterId, String groupCode, String ledgerIds) {
|
||||||
|
if (StrUtil.isBlank(matterId) || StrUtil.isBlank(groupCode)) {
|
||||||
|
return Result.error("参数错误");
|
||||||
|
}
|
||||||
|
List<String> ledgerIdList = parseLedgerIds(ledgerIds);
|
||||||
|
if (ledgerIdList.isEmpty()) {
|
||||||
|
return Result.error("请选择需要调整的教职工");
|
||||||
|
}
|
||||||
|
|
||||||
|
NutMap matter = fetchAssignMatter(matterId);
|
||||||
|
if (matter == null || matter.isEmpty()) {
|
||||||
|
return Result.error("事项不存在或线路信息不完整");
|
||||||
|
}
|
||||||
|
int maxGroupPeople = mapInt(matter, "maxGroupPeople");
|
||||||
|
if (maxGroupPeople <= 0) {
|
||||||
|
return Result.error("未配置最大成团人数,无法调整成团");
|
||||||
|
}
|
||||||
|
|
||||||
|
NutMap targetGroup = fetchTargetGroup(matterId, groupCode);
|
||||||
|
if (targetGroup == null || targetGroup.isEmpty()) {
|
||||||
|
return Result.error("请选择有效的成团信息");
|
||||||
|
}
|
||||||
|
List<SignupUnit> selectedUnits = querySelectedSignupUnits(matterId, ledgerIdList);
|
||||||
|
if (selectedUnits.size() != ledgerIdList.size()) {
|
||||||
|
return Result.error("所选人员信息无效,请刷新后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
int selectedPeopleCount = selectedUnits.stream().mapToInt(unit -> unit.peopleCount).sum();
|
||||||
|
int targetPeopleCount = countTargetGroupPeople(matterId, groupCode, ledgerIdList);
|
||||||
|
if (targetPeopleCount + selectedPeopleCount > maxGroupPeople) {
|
||||||
|
return Result.error("调整后目标团共" + (targetPeopleCount + selectedPeopleCount)
|
||||||
|
+ "人,超过最大成团人数" + maxGroupPeople + "人,无法调整");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLedgerGroup(matterId, ledgerIdList, groupCode, targetGroup.getString("groupName", ""));
|
||||||
|
return Result.success("调整成功,共调整" + selectedUnits.size() + "名教职工");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询事项基础信息,最大成团人数是自动分配的核心容量上限。
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 当前领办以团为单位执行,写入范围限定为同一事项、同一成团编码下的有效台账。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result undertakeGroup(String matterId, String groupCode, String unionId, String unionName) {
|
||||||
|
if (StrUtil.isBlank(matterId) || StrUtil.isBlank(groupCode) || StrUtil.isBlank(unionId)) {
|
||||||
|
return Result.error("参数错误");
|
||||||
|
}
|
||||||
|
NutMap group = fetchUndertakeGroup(matterId, groupCode);
|
||||||
|
if (group == null || group.isEmpty()) {
|
||||||
|
return Result.error("当前团不存在或未成团,无法领办");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(group.getString("undertakeUnionId", ""))) {
|
||||||
|
return Result.error("当前团已领办,不允许修改");
|
||||||
|
}
|
||||||
|
Cnd cnd = buildUndertakeGroupCnd(matterId, groupCode);
|
||||||
|
Chain chain = Chain.make("undertakeUnionId", unionId)
|
||||||
|
.add("undertakeUnionName", StrUtil.blankToDefault(unionName, ""));
|
||||||
|
dao().update(TourLedger.class, chain, cnd);
|
||||||
|
return Result.success(NutMap.NEW().addv("unionId", unionId).addv("unionName", StrUtil.blankToDefault(unionName, "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解除领办同样按团处理,仅清空当前团台账领办字段,保留事项表原有归属信息。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result releaseUndertakeGroup(String matterId, String groupCode) {
|
||||||
|
if (StrUtil.isBlank(matterId) || StrUtil.isBlank(groupCode)) {
|
||||||
|
return Result.error("参数错误");
|
||||||
|
}
|
||||||
|
NutMap group = fetchUndertakeGroup(matterId, groupCode);
|
||||||
|
if (group == null || group.isEmpty()) {
|
||||||
|
return Result.error("当前团不存在或未成团,无法解除");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(group.getString("undertakeUnionId", ""))) {
|
||||||
|
return Result.error("当前团未领办,无需解除");
|
||||||
|
}
|
||||||
|
Cnd cnd = buildUndertakeGroupCnd(matterId, groupCode);
|
||||||
|
dao().update(TourLedger.class, Chain.make("undertakeUnionId", "").add("undertakeUnionName", ""), cnd);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks the yearly setting row so concurrent out-province signup checks read updated quota counts one by one.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void lockTourSignupQuotaYear(Integer year) {
|
||||||
|
if (year == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// MySQL/InnoDB holds this row lock until the current signup transaction commits.
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT id
|
||||||
|
FROM tour_setting
|
||||||
|
WHERE delFlag = 0
|
||||||
|
AND `year` = @year
|
||||||
|
ORDER BY id
|
||||||
|
FOR UPDATE
|
||||||
|
""");
|
||||||
|
sql.setParam("year", year);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
dao().execute(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks one matter row so concurrent submissions for the same travel period check capacity one by one.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void lockTourSignupMatter(String matterId) {
|
||||||
|
if (StrUtil.isBlank(matterId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// MySQL/InnoDB holds this matter row lock until the current signup transaction commits.
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT id
|
||||||
|
FROM tour_matter
|
||||||
|
WHERE delFlag = 0
|
||||||
|
AND id = @matterId
|
||||||
|
FOR UPDATE
|
||||||
|
""");
|
||||||
|
sql.setParam("matterId", matterId);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
dao().execute(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the current time against the effective signup window used by tour signup and cancellation.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Result checkSignupTimeWindow(TourMatter matter, TourSetting setting) {
|
||||||
|
if (matter == null || setting == null) {
|
||||||
|
return Result.error("报名事项配置不存在或已停用");
|
||||||
|
}
|
||||||
|
String signupStartTime = StrUtil.blankToDefault(setting.getSignupStartTime(), matter.getSignupStartTime());
|
||||||
|
String signupEndTime = StrUtil.blankToDefault(setting.getSignupEndTime(), matter.getSignupEndTime());
|
||||||
|
if (StrUtil.isBlank(signupStartTime) || StrUtil.isBlank(signupEndTime)) {
|
||||||
|
return Result.error("报名时间未配置");
|
||||||
|
}
|
||||||
|
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
if (now.compareTo(normalizeSignupDateTime(signupStartTime, false)) < 0) {
|
||||||
|
return Result.error("报名未开始");
|
||||||
|
}
|
||||||
|
if (now.compareTo(normalizeSignupDateTime(signupEndTime, true)) > 0) {
|
||||||
|
return Result.error("报名已结束");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes date-only signup values so the end date includes the whole day.
|
||||||
|
*/
|
||||||
|
private String normalizeSignupDateTime(String value, boolean endOfDay) {
|
||||||
|
String text = StrUtil.blankToDefault(value, "").trim();
|
||||||
|
if (text.length() == 10) {
|
||||||
|
return text + (endOfDay ? " 23:59:59" : " 00:00:00");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps controller SQL ordering safe by resolving only known list fields.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getTourSignupOrderColumn(String orderName) {
|
||||||
|
if ("lineCode".equals(orderName)) {
|
||||||
|
return "CAST(l.lineCode AS UNSIGNED)";
|
||||||
|
}
|
||||||
|
if ("lineName".equals(orderName)) {
|
||||||
|
return "l.lineName";
|
||||||
|
}
|
||||||
|
if ("travelPeriod".equals(orderName)) {
|
||||||
|
return "m.travelStartTime";
|
||||||
|
}
|
||||||
|
if ("matterName".equals(orderName)) {
|
||||||
|
return "m.matterName";
|
||||||
|
}
|
||||||
|
if ("unionName".equals(orderName)) {
|
||||||
|
return "u.name";
|
||||||
|
}
|
||||||
|
if ("lineType".equals(orderName)) {
|
||||||
|
return "l.lineType";
|
||||||
|
}
|
||||||
|
if ("minGroupPeople".equals(orderName)) {
|
||||||
|
return "m.minGroupPeople";
|
||||||
|
}
|
||||||
|
if ("maxGroupPeople".equals(orderName)) {
|
||||||
|
return "m.maxGroupPeople";
|
||||||
|
}
|
||||||
|
if ("maxSignupPeople".equals(orderName)) {
|
||||||
|
return "m.maxSignupPeople";
|
||||||
|
}
|
||||||
|
if ("signupCount".equals(orderName)) {
|
||||||
|
return "signupCount";
|
||||||
|
}
|
||||||
|
return "CAST(l.lineCode AS UNSIGNED)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private NutMap fetchAssignMatter(String matterId) {
|
||||||
|
Cnd cnd = buildAssignMatterCnd(matterId);
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.maxGroupPeople
|
||||||
|
FROM tour_matter m
|
||||||
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
$condition
|
||||||
|
""");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
sql.setCallback(Sqls.callback.map());
|
||||||
|
dao().execute(sql);
|
||||||
|
return sql.getObject(NutMap.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前事项下已存在的目标成团信息,人员调整只能选择当前线路台账中已有的团。
|
||||||
|
*/
|
||||||
|
private NutMap fetchTargetGroup(String matterId, String groupCode) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
t.groupCode,
|
||||||
|
MAX(t.groupName) AS groupName
|
||||||
|
FROM tour_ledger t
|
||||||
|
WHERE t.delFlag = 0
|
||||||
|
AND t.matterId = @matterId
|
||||||
|
AND t.groupCode = @groupCode
|
||||||
|
GROUP BY t.groupCode
|
||||||
|
""");
|
||||||
|
sql.setParam("matterId", matterId);
|
||||||
|
sql.setParam("groupCode", groupCode);
|
||||||
|
sql.setCallback(Sqls.callback.map());
|
||||||
|
dao().execute(sql);
|
||||||
|
return sql.getObject(NutMap.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造自动分配事项查询条件,确保自动分配的数据范围与线路成团查看列表保持一致。
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 查询当前事项下指定团的领办字段,供领办和解除领办前置校验使用。
|
||||||
|
*/
|
||||||
|
private NutMap fetchUndertakeGroup(String matterId, String groupCode) {
|
||||||
|
Cnd cnd = buildAssignMatterCnd(matterId);
|
||||||
|
cnd.and("t.delFlag", "=", false);
|
||||||
|
cnd.and("t.groupCode", "=", groupCode);
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
t.groupCode,
|
||||||
|
MAX(t.groupName) AS groupName,
|
||||||
|
MAX(t.undertakeUnionId) AS undertakeUnionId,
|
||||||
|
MAX(t.undertakeUnionName) AS undertakeUnionName
|
||||||
|
FROM tour_ledger t
|
||||||
|
INNER JOIN tour_matter m ON m.id = t.matterId
|
||||||
|
INNER JOIN tour_line l ON l.id = m.lineId
|
||||||
|
$condition
|
||||||
|
GROUP BY t.groupCode
|
||||||
|
""");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
sql.setCallback(Sqls.callback.map());
|
||||||
|
dao().execute(sql);
|
||||||
|
return sql.getObject(NutMap.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造当前团领办字段更新范围,避免同一事项下其它团被误更新。
|
||||||
|
*/
|
||||||
|
private Cnd buildUndertakeGroupCnd(String matterId, String groupCode) {
|
||||||
|
return Cnd.where("matterId", "=", matterId)
|
||||||
|
.and("groupCode", "=", groupCode)
|
||||||
|
.and("delFlag", "=", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cnd buildAssignMatterCnd(String matterId) {
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
cnd.and("m.delFlag", "=", false);
|
||||||
|
cnd.and("m.enabled", "=", true);
|
||||||
|
cnd.and("m.id", "=", matterId);
|
||||||
|
cnd.and("m.lineId", "IS NOT", null);
|
||||||
|
cnd.and("m.lineId", "<>", "");
|
||||||
|
cnd.and("l.delFlag", "=", false);
|
||||||
|
cnd.and("l.enabled", "=", true);
|
||||||
|
applyMatterDataScope(cnd);
|
||||||
|
return cnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询已存在成团及当前占用人数,后续未成团人员优先从最早团开始补足。
|
||||||
|
*/
|
||||||
|
private List<GroupBucket> queryGroupBuckets(String matterId) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
t.groupCode,
|
||||||
|
MAX(t.groupName) AS groupName,
|
||||||
|
SUM(1 + IFNULL(f.familyCount, 0)) AS peopleCount
|
||||||
|
FROM tour_ledger t
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
|
FROM tour_ledger_family
|
||||||
|
WHERE delFlag = 0
|
||||||
|
GROUP BY ledgerId
|
||||||
|
) f ON f.ledgerId = t.id
|
||||||
|
WHERE t.delFlag = 0
|
||||||
|
AND t.matterId = @matterId
|
||||||
|
AND t.groupCode IS NOT NULL
|
||||||
|
AND t.groupCode <> ''
|
||||||
|
GROUP BY t.groupCode
|
||||||
|
ORDER BY t.groupCode ASC
|
||||||
|
""");
|
||||||
|
sql.setParam("matterId", matterId);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
dao().execute(sql);
|
||||||
|
|
||||||
|
List<GroupBucket> groups = new ArrayList<>();
|
||||||
|
for (NutMap item : sql.getList(NutMap.class)) {
|
||||||
|
GroupBucket group = new GroupBucket();
|
||||||
|
group.groupCode = item.getString("groupCode", "");
|
||||||
|
group.groupName = item.getString("groupName", "");
|
||||||
|
group.peopleCount = mapInt(item, "peopleCount");
|
||||||
|
groups.add(group);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询未成团教职工台账,并合并家属人数,保证教职工和家属作为一个整体分配。
|
||||||
|
*/
|
||||||
|
private List<SignupUnit> queryUnassignedSignupUnits(String matterId) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
t.id AS ledgerId,
|
||||||
|
t.userName,
|
||||||
|
1 + IFNULL(f.familyCount, 0) AS peopleCount
|
||||||
|
FROM tour_ledger t
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
|
FROM tour_ledger_family
|
||||||
|
WHERE delFlag = 0
|
||||||
|
GROUP BY ledgerId
|
||||||
|
) f ON f.ledgerId = t.id
|
||||||
|
WHERE t.delFlag = 0
|
||||||
|
AND t.matterId = @matterId
|
||||||
|
AND (t.groupCode IS NULL OR t.groupCode = '')
|
||||||
|
ORDER BY t.signupTime ASC, t.createdAt ASC
|
||||||
|
""");
|
||||||
|
sql.setParam("matterId", matterId);
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
dao().execute(sql);
|
||||||
|
|
||||||
|
List<SignupUnit> signupUnits = new ArrayList<>();
|
||||||
|
for (NutMap item : sql.getList(NutMap.class)) {
|
||||||
|
SignupUnit unit = new SignupUnit();
|
||||||
|
unit.ledgerId = item.getString("ledgerId", "");
|
||||||
|
unit.userName = item.getString("userName", "");
|
||||||
|
unit.peopleCount = mapInt(item, "peopleCount");
|
||||||
|
signupUnits.add(unit);
|
||||||
|
}
|
||||||
|
return signupUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询选中教职工台账及其家属人数,确保只调整当前事项下有效的教职工报名记录。
|
||||||
|
*/
|
||||||
|
private List<SignupUnit> querySelectedSignupUnits(String matterId, List<String> ledgerIds) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
t.id AS ledgerId,
|
||||||
|
t.userName,
|
||||||
|
1 + IFNULL(f.familyCount, 0) AS peopleCount
|
||||||
|
FROM tour_ledger t
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
|
FROM tour_ledger_family
|
||||||
|
WHERE delFlag = 0
|
||||||
|
GROUP BY ledgerId
|
||||||
|
) f ON f.ledgerId = t.id
|
||||||
|
WHERE t.delFlag = 0
|
||||||
|
AND t.matterId = @matterId
|
||||||
|
AND t.id IN (@ledgerIds)
|
||||||
|
ORDER BY t.signupTime ASC, t.createdAt ASC
|
||||||
|
""");
|
||||||
|
sql.setParam("matterId", matterId);
|
||||||
|
sql.setParam("ledgerIds", ledgerIds.toArray(new String[0]));
|
||||||
|
sql.setCallback(Sqls.callback.maps());
|
||||||
|
dao().execute(sql);
|
||||||
|
|
||||||
|
List<SignupUnit> signupUnits = new ArrayList<>();
|
||||||
|
for (NutMap item : sql.getList(NutMap.class)) {
|
||||||
|
SignupUnit unit = new SignupUnit();
|
||||||
|
unit.ledgerId = item.getString("ledgerId", "");
|
||||||
|
unit.userName = item.getString("userName", "");
|
||||||
|
unit.peopleCount = mapInt(item, "peopleCount");
|
||||||
|
signupUnits.add(unit);
|
||||||
|
}
|
||||||
|
return signupUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计目标团调整前人数,排除本次选中人员,避免原本已在目标团的人员被重复计数。
|
||||||
|
*/
|
||||||
|
private int countTargetGroupPeople(String matterId, String groupCode, List<String> excludeLedgerIds) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS peopleCount
|
||||||
|
FROM tour_ledger t
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ledgerId, COUNT(1) AS familyCount
|
||||||
|
FROM tour_ledger_family
|
||||||
|
WHERE delFlag = 0
|
||||||
|
GROUP BY ledgerId
|
||||||
|
) f ON f.ledgerId = t.id
|
||||||
|
WHERE t.delFlag = 0
|
||||||
|
AND t.matterId = @matterId
|
||||||
|
AND t.groupCode = @groupCode
|
||||||
|
AND t.id NOT IN (@excludeLedgerIds)
|
||||||
|
""");
|
||||||
|
sql.setParam("matterId", matterId);
|
||||||
|
sql.setParam("groupCode", groupCode);
|
||||||
|
sql.setParam("excludeLedgerIds", excludeLedgerIds.toArray(new String[0]));
|
||||||
|
sql.setCallback(Sqls.callback.map());
|
||||||
|
dao().execute(sql);
|
||||||
|
return mapInt(sql.getObject(NutMap.class), "peopleCount");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从最早的团开始查找可容纳当前报名单位的团,避免前面团未满但后面新建团的情况。
|
||||||
|
*/
|
||||||
|
private GroupBucket findAvailableGroup(List<GroupBucket> groups, int peopleCount, int maxGroupPeople) {
|
||||||
|
for (GroupBucket group : groups) {
|
||||||
|
if (group.peopleCount + peopleCount <= maxGroupPeople) {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据现有最大团序号生成下一个团序号,兼容已有非标准团编码。
|
||||||
|
*/
|
||||||
|
private int nextGroupIndex(List<GroupBucket> groups) {
|
||||||
|
int maxIndex = 0;
|
||||||
|
for (GroupBucket group : groups) {
|
||||||
|
maxIndex = Math.max(maxIndex, parseGroupIndex(group.groupCode));
|
||||||
|
}
|
||||||
|
return maxIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新的成团桶,编码使用T001递增,名称使用中文序号加“团”。
|
||||||
|
*/
|
||||||
|
private GroupBucket createGroupBucket(int index) {
|
||||||
|
GroupBucket group = new GroupBucket();
|
||||||
|
group.groupCode = String.format("T%03d", index);
|
||||||
|
group.groupName = toChineseNumber(index) + "团";
|
||||||
|
group.peopleCount = 0;
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动分配按事项归属控制数据范围:校工会查看全部;分工会查看本工会事项;其它角色仅处理自己创建的事项。
|
||||||
|
*/
|
||||||
|
private void applyMatterDataScope(Cnd cnd) {
|
||||||
|
if (hasSchoolUnionScope()
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|
||||||
|
|| hasTourApplicantScope()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasBranchUnionScope()) {
|
||||||
|
cnd.and("m.unionId", "=", SecurityUtil.getUnionId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SqlExpressionGroup creatorGroup = new SqlExpressionGroup();
|
||||||
|
creatorGroup.or("m.creatorUserId", "=", SecurityUtil.getUserId());
|
||||||
|
creatorGroup.or("m.createdBy", "=", SecurityUtil.getUserId());
|
||||||
|
cnd.and(creatorGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否具备校工会级别的事项处理范围。
|
||||||
|
*/
|
||||||
|
private boolean hasSchoolUnionScope() {
|
||||||
|
return AuthUtil.hasRole(RoleConstant.SYSADMIN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CHAIRMAN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_VICE_CHAIRMAN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_WELFARE_ADMIN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_MEMBER_ADMIN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_TC_ADMIN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_WC_ADMIN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_DC_ADMIN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_PROPOSAL_ADMIN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 疗休养领办人需要与领办列表保持一致,可在领办前查询到全部成团数据。
|
||||||
|
*/
|
||||||
|
private boolean hasTourApplicantScope() {
|
||||||
|
return AuthUtil.hasRole(RoleConstant.TOUR_APPLICANT.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否具备分工会级别的事项处理范围。
|
||||||
|
*/
|
||||||
|
private boolean hasBranchUnionScope() {
|
||||||
|
return AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_VICE_CHAIRMAN.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_OPERATOR.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_GROUP_LEADER.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ARTICLE_WRITER.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WY.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ZUZHI_WY.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_XUANCHUAN_WY.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_NVGONG_WY.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_QINGNIAN_WY.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_WY.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_SHENGGHUO_WY.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_TIAOJIE_WY.name())
|
||||||
|
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_SPORTS.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新教职工台账成团信息,家属行展示时继承教职工台账的成团信息。
|
||||||
|
*/
|
||||||
|
private void updateLedgerGroup(String ledgerId, String groupCode, String groupName) {
|
||||||
|
dao().update(TourLedger.class, Chain.make("groupCode", groupCode).add("groupName", groupName), Cnd.where("id", "=", ledgerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新选中的教职工台账成团信息,人员调整只写教职工台账,家属展示继续继承教职工。
|
||||||
|
*/
|
||||||
|
private void updateLedgerGroup(String matterId, List<String> ledgerIds, String groupCode, String groupName) {
|
||||||
|
Cnd cnd = Cnd.where("id", "in", ledgerIds)
|
||||||
|
.and("matterId", "=", matterId)
|
||||||
|
.and("delFlag", "=", false);
|
||||||
|
dao().update(TourLedger.class, Chain.make("groupCode", groupCode).add("groupName", groupName), cnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析前端逗号分隔的台账ID,去重并过滤空值,避免重复计数和重复更新。
|
||||||
|
*/
|
||||||
|
private List<String> parseLedgerIds(String ledgerIds) {
|
||||||
|
Set<String> idSet = new LinkedHashSet<>();
|
||||||
|
if (StrUtil.isBlank(ledgerIds)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
for (String id : ledgerIds.split(",")) {
|
||||||
|
if (StrUtil.isNotBlank(id)) {
|
||||||
|
idSet.add(id.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(idSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析T001这类标准团编码中的数字序号,非标准编码返回0。
|
||||||
|
*/
|
||||||
|
private int parseGroupIndex(String groupCode) {
|
||||||
|
if (StrUtil.isBlank(groupCode) || !groupCode.startsWith("T")) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(groupCode.substring(1));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数字序号转为中文团名序号,支持常见的两位和多位团序号展示。
|
||||||
|
*/
|
||||||
|
private String toChineseNumber(int number) {
|
||||||
|
String[] digits = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"};
|
||||||
|
if (number <= 0) {
|
||||||
|
return String.valueOf(number);
|
||||||
|
}
|
||||||
|
if (number < 10) {
|
||||||
|
return "第" + digits[number];
|
||||||
|
}
|
||||||
|
if (number < 100) {
|
||||||
|
int ten = number / 10;
|
||||||
|
int one = number % 10;
|
||||||
|
String prefix = ten == 1 ? "十" : digits[ten] + "十";
|
||||||
|
return "第" + prefix + (one == 0 ? "" : digits[one]);
|
||||||
|
}
|
||||||
|
String value = String.valueOf(number);
|
||||||
|
StringBuilder builder = new StringBuilder("第");
|
||||||
|
for (int i = 0; i < value.length(); i++) {
|
||||||
|
builder.append(digits[value.charAt(i) - '0']);
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全读取SQL返回的整数值,避免不同数据库驱动返回Number或字符串时转换失败。
|
||||||
|
*/
|
||||||
|
private int mapInt(NutMap map, String key) {
|
||||||
|
Object value = map == null ? null : map.get(key);
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number.intValue();
|
||||||
|
}
|
||||||
|
if (value == null || StrUtil.isBlank(String.valueOf(value))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Integer.parseInt(String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已有成团容量桶,记录每个团当前人数以及后续分配后的占用人数。
|
||||||
|
*/
|
||||||
|
private static class GroupBucket {
|
||||||
|
private String groupCode;
|
||||||
|
private String groupName;
|
||||||
|
private int peopleCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待分配报名单位,教职工及其家属作为不可拆分的最小分配单位。
|
||||||
|
*/
|
||||||
|
private static class SignupUnit {
|
||||||
|
private String ledgerId;
|
||||||
|
private String userName;
|
||||||
|
private int peopleCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -3,13 +3,35 @@ package com.budwk.app.zhgh.dayofficework.tour.service.impl;
|
|||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
|
||||||
|
import org.nutz.dao.Chain;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
public class TourMatterServiceImpl extends BaseServiceImpl<TourMatter> implements TourMatterService {
|
public class TourMatterServiceImpl extends BaseServiceImpl<TourMatter> implements TourMatterService {
|
||||||
|
|
||||||
public TourMatterServiceImpl(Dao dao) {
|
public TourMatterServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按事项ID批量更新报名起止时间,供事项列表“一键赋值报名时间”使用。
|
||||||
|
*
|
||||||
|
* @param ids 当前查询条件命中的事项ID集合
|
||||||
|
* @param signupStartTime 报名开始时间
|
||||||
|
* @param signupEndTime 报名结束时间
|
||||||
|
* @return 实际更新数量
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int assignSignupTime(List<String> ids, String signupStartTime, String signupEndTime) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return dao().update(TourMatter.class,
|
||||||
|
Chain.make("signupStartTime", signupStartTime).add("signupEndTime", signupEndTime),
|
||||||
|
Cnd.where("id", "in", ids));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+358
@@ -0,0 +1,358 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
|
import com.budwk.app.sys.views.View_user;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourParticipantRoster;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
|
||||||
|
import org.nutz.dao.Chain;
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 疗休养人员名单维护服务实现。
|
||||||
|
* 负责同步配置可参与人员、回填台账参加状态,并保留已有台账人员的历史统计口径。
|
||||||
|
*/
|
||||||
|
@IocBean(args = {"refer:dao"})
|
||||||
|
public class TourParticipantRosterServiceImpl extends BaseServiceImpl<TourParticipantRoster> implements TourParticipantRosterService {
|
||||||
|
|
||||||
|
private static final int USER_QUERY_BATCH_SIZE = 500;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ActivityBasicScopeService activityBasicScopeService;
|
||||||
|
|
||||||
|
public TourParticipantRosterServiceImpl(Dao dao) {
|
||||||
|
super(dao);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取配置可参与人员组别,并写入或刷新人员名单基础信息。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void syncSettingRoster(String settingId) {
|
||||||
|
if (StrUtil.isBlank(settingId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TourSetting setting = dao().fetch(TourSetting.class, settingId);
|
||||||
|
if (setting == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (View_user user : listEligibleUsers(setting)) {
|
||||||
|
upsertRosterUser(settingId, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置普通保存时只做懒加载:配置下已有名单数据则不重新拉取人员。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void syncSettingRosterIfEmpty(String settingId) {
|
||||||
|
if (StrUtil.isBlank(settingId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int rosterCount = count(Cnd.where("settingId", "=", settingId));
|
||||||
|
if (rosterCount > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
syncSettingRoster(settingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在一个事务内重建人员名单:删除旧名单、重载当前可参与人员、回放有效台账参加状态。
|
||||||
|
* 若台账人员已不在当前可参与人员中,仍保留在名单中,并返回提示给页面展示。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public List<String> rebuildSettingRosters(List<String> settingIds) {
|
||||||
|
List<String> warningMessages = new ArrayList<>();
|
||||||
|
if (CollUtil.isEmpty(settingIds)) {
|
||||||
|
return warningMessages;
|
||||||
|
}
|
||||||
|
for (String settingId : settingIds) {
|
||||||
|
if (StrUtil.isBlank(settingId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TourSetting setting = dao().fetch(TourSetting.class, settingId);
|
||||||
|
if (setting == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<View_user> eligibleUsers = listEligibleUsers(setting);
|
||||||
|
Set<String> eligibleJobNos = collectEligibleJobNos(eligibleUsers);
|
||||||
|
clear(Cnd.where("settingId", "=", settingId));
|
||||||
|
for (View_user user : eligibleUsers) {
|
||||||
|
upsertRosterUser(settingId, user);
|
||||||
|
}
|
||||||
|
List<String> retainedUserNames = replayLedgerJoinedState(settingId, eligibleJobNos);
|
||||||
|
if (CollUtil.isNotEmpty(retainedUserNames)) {
|
||||||
|
warningMessages.add(buildRetainedLedgerUserMessage(setting, retainedUserNames));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return warningMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除配置下的人员名单,避免已删除配置继续参与统计查询。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void clearSettingRoster(String settingId) {
|
||||||
|
if (StrUtil.isBlank(settingId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clear(Cnd.where("settingId", "=", settingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报名成功和修改报名都会将人员名单标记为已参加,并指向当前有效台账。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void markJoinedByLedger(TourLedger ledger) {
|
||||||
|
String settingId = resolveSettingId(ledger);
|
||||||
|
if (StrUtil.isBlank(settingId) || ledger == null || StrUtil.isBlank(ledger.getJobNo())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TourParticipantRoster roster = fetchRoster(settingId, ledger.getJobNo());
|
||||||
|
if (roster == null) {
|
||||||
|
roster = new TourParticipantRoster();
|
||||||
|
roster.setSettingId(settingId);
|
||||||
|
roster.setJobNo(ledger.getJobNo());
|
||||||
|
roster.setJoined(true);
|
||||||
|
fillRosterFromLedger(roster, ledger);
|
||||||
|
insert(roster);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Chain chain = Chain.make("joined", true)
|
||||||
|
.add("ledgerId", StrUtil.blankToDefault(ledger.getId(), ""))
|
||||||
|
.add("userName", StrUtil.blankToDefault(ledger.getUserName(), roster.getUserName()))
|
||||||
|
.add("unitId", StrUtil.blankToDefault(ledger.getUnitId(), roster.getUnitId()))
|
||||||
|
.add("unitName", StrUtil.blankToDefault(ledger.getUnitName(), roster.getUnitName()))
|
||||||
|
.add("unionId", StrUtil.blankToDefault(ledger.getUnionId(), roster.getUnionId()))
|
||||||
|
.add("unionName", StrUtil.blankToDefault(ledger.getUnionName(), roster.getUnionName()));
|
||||||
|
dao().update(TourParticipantRoster.class, chain, Cnd.where("id", "=", roster.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消报名只重置当前人员名单状态,历史台账删除记录仍保留在 tour_ledger 中。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void markNotJoinedByLedger(TourLedger ledger) {
|
||||||
|
String settingId = resolveSettingId(ledger);
|
||||||
|
if (StrUtil.isBlank(settingId) || ledger == null || StrUtil.isBlank(ledger.getJobNo())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TourParticipantRoster roster = fetchRoster(settingId, ledger.getJobNo());
|
||||||
|
if (roster == null) {
|
||||||
|
roster = new TourParticipantRoster();
|
||||||
|
roster.setSettingId(settingId);
|
||||||
|
roster.setJobNo(ledger.getJobNo());
|
||||||
|
roster.setJoined(false);
|
||||||
|
fillRosterFromLedger(roster, ledger);
|
||||||
|
roster.setLedgerId("");
|
||||||
|
insert(roster);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dao().update(TourParticipantRoster.class,
|
||||||
|
Chain.make("joined", false).add("ledgerId", ""),
|
||||||
|
Cnd.where("id", "=", roster.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将配置中以文本保存的可参与人员组别 id 转换为人员范围服务需要的整数。
|
||||||
|
*/
|
||||||
|
private Integer parseGroupId(String activityGroupId) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(activityGroupId);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据配置的可参与人员组别加载人员基础数据。
|
||||||
|
* 这里集中处理空配置和无效组别,方便同步和重建流程复用同一口径。
|
||||||
|
*/
|
||||||
|
private List<View_user> listEligibleUsers(TourSetting setting) {
|
||||||
|
if (setting == null || StrUtil.isBlank(setting.getActivityGroupId())) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
Integer groupId = parseGroupId(setting.getActivityGroupId());
|
||||||
|
if (groupId == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<String> userIds = activityBasicScopeService.listGroupUserIds(groupId);
|
||||||
|
if (CollUtil.isEmpty(userIds)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<View_user> users = new ArrayList<>();
|
||||||
|
for (View_user user : queryUsers(userIds)) {
|
||||||
|
if (user != null && StrUtil.isNotBlank(user.getLoginname())) {
|
||||||
|
users.add(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取当前可参与人员工号集合,用于判断已有台账人员是否已被移出可参与范围。
|
||||||
|
*/
|
||||||
|
private Set<String> collectEligibleJobNos(List<View_user> users) {
|
||||||
|
Set<String> jobNos = new HashSet<>();
|
||||||
|
if (CollUtil.isEmpty(users)) {
|
||||||
|
return jobNos;
|
||||||
|
}
|
||||||
|
for (View_user user : users) {
|
||||||
|
if (user != null && StrUtil.isNotBlank(user.getLoginname())) {
|
||||||
|
jobNos.add(user.getLoginname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jobNos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分批查询用户视图,避免大范围人员组别生成过长的 SQL IN 条件。
|
||||||
|
*/
|
||||||
|
private List<View_user> queryUsers(List<String> userIds) {
|
||||||
|
List<View_user> users = new ArrayList<>();
|
||||||
|
for (int i = 0; i < userIds.size(); i += USER_QUERY_BATCH_SIZE) {
|
||||||
|
int end = Math.min(i + USER_QUERY_BATCH_SIZE, userIds.size());
|
||||||
|
List<String> batch = userIds.subList(i, end);
|
||||||
|
users.addAll(dao().query(View_user.class, Cnd.where("id", "in", batch)));
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增可参与人员,或刷新已存在人员的基础组织信息。
|
||||||
|
*/
|
||||||
|
private void upsertRosterUser(String settingId, View_user user) {
|
||||||
|
TourParticipantRoster roster = fetchRoster(settingId, user.getLoginname());
|
||||||
|
if (roster == null) {
|
||||||
|
roster = new TourParticipantRoster();
|
||||||
|
roster.setSettingId(settingId);
|
||||||
|
roster.setJobNo(user.getLoginname());
|
||||||
|
roster.setJoined(false);
|
||||||
|
fillRosterFromUser(roster, user);
|
||||||
|
insert(roster);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dao().update(TourParticipantRoster.class,
|
||||||
|
Chain.make("userName", StrUtil.blankToDefault(user.getUsername(), ""))
|
||||||
|
.add("unitId", StrUtil.blankToDefault(user.getUnitId(), ""))
|
||||||
|
.add("unitName", StrUtil.blankToDefault(user.getUnitName(), ""))
|
||||||
|
.add("unionId", StrUtil.blankToDefault(user.getUnionId(), ""))
|
||||||
|
.add("unionName", StrUtil.blankToDefault(user.getUnionName(), "")),
|
||||||
|
Cnd.where("id", "=", roster.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回放配置下有效疗休养台账,使重建后的名单保持当前报名状态。
|
||||||
|
* 对已不在可参与人员中但存在台账的人员,继续保留名单记录并返回人员名称用于页面提示。
|
||||||
|
*/
|
||||||
|
private List<String> replayLedgerJoinedState(String settingId, Set<String> eligibleJobNos) {
|
||||||
|
Set<String> retainedUserNames = new LinkedHashSet<>();
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT t.*
|
||||||
|
FROM tour_ledger t
|
||||||
|
INNER JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
|
||||||
|
WHERE t.delFlag = 0
|
||||||
|
AND m.settingId = @settingId
|
||||||
|
AND IFNULL(t.jobNo, '') <> ''
|
||||||
|
ORDER BY t.signupTime DESC, t.createdAt DESC
|
||||||
|
""");
|
||||||
|
sql.setParam("settingId", settingId);
|
||||||
|
sql.setEntity(dao().getEntity(TourLedger.class));
|
||||||
|
sql.setCallback(Sqls.callback.entities());
|
||||||
|
dao().execute(sql);
|
||||||
|
|
||||||
|
Set<String> handledJobNos = new HashSet<>();
|
||||||
|
for (TourLedger ledger : sql.getList(TourLedger.class)) {
|
||||||
|
if (ledger == null || StrUtil.isBlank(ledger.getJobNo()) || handledJobNos.contains(ledger.getJobNo())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
handledJobNos.add(ledger.getJobNo());
|
||||||
|
if (eligibleJobNos == null || !eligibleJobNos.contains(ledger.getJobNo())) {
|
||||||
|
retainedUserNames.add(displayLedgerUserName(ledger));
|
||||||
|
}
|
||||||
|
markJoinedByLedger(ledger);
|
||||||
|
}
|
||||||
|
return new ArrayList<>(retainedUserNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按配置和工号查找有效人员名单记录,这两个字段是业务逻辑唯一键。
|
||||||
|
*/
|
||||||
|
private TourParticipantRoster fetchRoster(String settingId, String jobNo) {
|
||||||
|
return fetch(Cnd.where("settingId", "=", settingId)
|
||||||
|
.and("jobNo", "=", jobNo)
|
||||||
|
.and("delFlag", "=", false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过台账关联的事项解析配置 id,因为 tour_ledger 本身不直接保存 settingId。
|
||||||
|
*/
|
||||||
|
private String resolveSettingId(TourLedger ledger) {
|
||||||
|
if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
TourMatter matter = dao().fetch(TourMatter.class, ledger.getMatterId());
|
||||||
|
return matter == null ? "" : matter.getSettingId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从用户视图复制人员基础信息到名单记录。
|
||||||
|
*/
|
||||||
|
private void fillRosterFromUser(TourParticipantRoster roster, View_user user) {
|
||||||
|
roster.setUserName(StrUtil.blankToDefault(user.getUsername(), ""));
|
||||||
|
roster.setUnitId(StrUtil.blankToDefault(user.getUnitId(), ""));
|
||||||
|
roster.setUnitName(StrUtil.blankToDefault(user.getUnitName(), ""));
|
||||||
|
roster.setUnionId(StrUtil.blankToDefault(user.getUnionId(), ""));
|
||||||
|
roster.setUnionName(StrUtil.blankToDefault(user.getUnionName(), ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当名单同步尚未创建该人员时,从台账复制人员和组织信息补入名单。
|
||||||
|
*/
|
||||||
|
private void fillRosterFromLedger(TourParticipantRoster roster, TourLedger ledger) {
|
||||||
|
roster.setUserName(StrUtil.blankToDefault(ledger.getUserName(), ""));
|
||||||
|
roster.setUnitId(StrUtil.blankToDefault(ledger.getUnitId(), ""));
|
||||||
|
roster.setUnitName(StrUtil.blankToDefault(ledger.getUnitName(), ""));
|
||||||
|
roster.setUnionId(StrUtil.blankToDefault(ledger.getUnionId(), ""));
|
||||||
|
roster.setUnionName(StrUtil.blankToDefault(ledger.getUnionName(), ""));
|
||||||
|
roster.setLedgerId(StrUtil.blankToDefault(ledger.getId(), ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造“有台账但已不在可参与人员范围中”的配置级提示。
|
||||||
|
*/
|
||||||
|
private String buildRetainedLedgerUserMessage(TourSetting setting, List<String> userNames) {
|
||||||
|
return StrUtil.blankToDefault(setting.getConfigName(), "未命名配置")
|
||||||
|
+ "(" + setting.getYear() + "):【" + String.join("、", userNames)
|
||||||
|
+ "】已不在“可参与人员”中,但已生成台账,无法剔除这类人员";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先显示台账姓名,姓名为空时使用工号,保证提示中始终能定位到具体人员。
|
||||||
|
*/
|
||||||
|
private String displayLedgerUserName(TourLedger ledger) {
|
||||||
|
if (StrUtil.isNotBlank(ledger.getUserName())) {
|
||||||
|
return ledger.getUserName();
|
||||||
|
}
|
||||||
|
return StrUtil.blankToDefault(ledger.getJobNo(), "未知人员");
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -1,8 +1,11 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
|
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
|
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
|
||||||
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
|
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
|
||||||
|
import org.nutz.dao.Chain;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
|
||||||
@@ -12,4 +15,19 @@ public class TourSettingServiceImpl extends BaseServiceImpl<TourSetting> impleme
|
|||||||
public TourSettingServiceImpl(Dao dao) {
|
public TourSettingServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新疗休养配置,并对报名时间做显式空值覆盖。
|
||||||
|
*
|
||||||
|
* @param tourSetting 页面提交的疗休养配置
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateWithNullableSignupTime(TourSetting tourSetting) {
|
||||||
|
updateIgnoreNull(tourSetting);
|
||||||
|
// updateIgnoreNull 会忽略 null;报名时间允许清空时,需要额外将空值覆盖到数据库。
|
||||||
|
dao().update(TourSetting.class,
|
||||||
|
Chain.make("signupStartTime", StrUtil.blankToDefault(tourSetting.getSignupStartTime(), ""))
|
||||||
|
.add("signupEndTime", StrUtil.blankToDefault(tourSetting.getSignupEndTime(), "")),
|
||||||
|
Cnd.where(TourSetting::getId, "=", tourSetting.getId()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import javax.validation.Valid;
|
|||||||
@At("/platform/TMap")
|
@At("/platform/TMap")
|
||||||
public class TMapController {
|
public class TMapController {
|
||||||
|
|
||||||
private static String KEY = "MLLBZ-GQECI-ASXG7-5GNOZ-XW2OF-H5BVH";
|
private static String KEY = "NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU";
|
||||||
@Inject
|
@Inject
|
||||||
private SysConfigService configService;
|
private SysConfigService configService;
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -99,10 +99,13 @@ public class DifficultHelpMineController {
|
|||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
pageParam.buildSearch(cnd, "info.");
|
pageParam.buildSearch(cnd, "info.");
|
||||||
if (StrUtil.isAllBlank(pageParam.getPageOrderName(), pageParam.getPageOrderBy())) {
|
// 排序字段必须经过 service 白名单校验,避免前端 pageOrderName 直接进入 ORDER BY。
|
||||||
|
String orderColumn = difficultHelpCommonService.getSafeMineOrderColumn(pageParam.getPageOrderName());
|
||||||
|
String orderBy = PageUtil.getOrder(pageParam.getPageOrderBy());
|
||||||
|
if (StrUtil.isBlank(orderColumn) || StrUtil.isBlank(orderBy)) {
|
||||||
cnd.desc("info.applyTime");
|
cnd.desc("info.applyTime");
|
||||||
} else {
|
} else {
|
||||||
cnd.orderBy("info." + pageParam.getPageOrderName(), PageUtil.getOrder(pageParam.getPageOrderBy()));
|
cnd.orderBy(orderColumn, orderBy);
|
||||||
}
|
}
|
||||||
cnd.groupBy("info.id");
|
cnd.groupBy("info.id");
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
|
|||||||
+8
@@ -11,4 +11,12 @@ import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
|
|||||||
* @注释
|
* @注释
|
||||||
*/
|
*/
|
||||||
public interface DifficultHelpCommonService extends BaseService<DifficultHelpInfo> {
|
public interface DifficultHelpCommonService extends BaseService<DifficultHelpInfo> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验困难帮扶“我的申请”列表排序字段,并返回可用于 SQL 排序的安全字段名。
|
||||||
|
*
|
||||||
|
* @param pageOrderName 前端传入的排序字段
|
||||||
|
* @return 白名单内的 SQL 排序字段;不在白名单时返回空字符串
|
||||||
|
*/
|
||||||
|
String getSafeMineOrderColumn(String pageOrderName);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
@@ -1,5 +1,6 @@
|
|||||||
package com.budwk.app.zhgh.staffbenefit.difficulthelp.service.impl;
|
package com.budwk.app.zhgh.staffbenefit.difficulthelp.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
|
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
|
||||||
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
|
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
|
||||||
@@ -19,4 +20,28 @@ public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHel
|
|||||||
public DifficultHelpCommonServiceImpl(Dao dao) {
|
public DifficultHelpCommonServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验困难帮扶“我的申请”列表排序字段,避免前端参数直接拼接到 ORDER BY。
|
||||||
|
*
|
||||||
|
* @param pageOrderName 前端传入的排序字段
|
||||||
|
* @return 白名单内的 SQL 排序字段;不在白名单时返回空字符串
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getSafeMineOrderColumn(String pageOrderName) {
|
||||||
|
if (StrUtil.isBlank(pageOrderName)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅允许页面表格中存在的可排序字段参与 ORDER BY,未知字段交由 controller 回退默认排序。
|
||||||
|
return switch (pageOrderName) {
|
||||||
|
case "loginName" -> "info.loginName";
|
||||||
|
case "userName" -> "info.userName";
|
||||||
|
case "unionName" -> "info.unionName";
|
||||||
|
case "unitName" -> "info.unitName";
|
||||||
|
case "subsidyStandards" -> "info.subsidyStandards";
|
||||||
|
case "applyTime" -> "info.applyTime";
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -108,10 +108,13 @@ public class MemberApplyMineController {
|
|||||||
cnd.and("info.userId", "=", SecurityUtil.getUserId());
|
cnd.and("info.userId", "=", SecurityUtil.getUserId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
// 排序字段必须经过 service 白名单校验,避免前端 pageOrderName 直接进入 ORDER BY。
|
||||||
|
String orderColumn = memberCommonService.getSafeMineOrderColumn(pageForm.getPageOrderName());
|
||||||
|
String orderBy = PageUtil.getOrder(pageForm.getPageOrderBy());
|
||||||
|
if (StrUtil.isBlank(orderColumn) || StrUtil.isBlank(orderBy)) {
|
||||||
cnd.desc("info.applyDateTime");
|
cnd.desc("info.applyDateTime");
|
||||||
} else {
|
} else {
|
||||||
cnd.orderBy("info." + pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
|
cnd.orderBy(orderColumn, orderBy);
|
||||||
}
|
}
|
||||||
cnd.groupBy("t.id");
|
cnd.groupBy("t.id");
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
|
|||||||
+5
-2
@@ -111,10 +111,13 @@ public class MemberChangeMineController {
|
|||||||
cnd.and("info.userId", "=", SecurityUtil.getUserId());
|
cnd.and("info.userId", "=", SecurityUtil.getUserId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
// 排序字段必须经过 service 白名单校验,避免前端 pageOrderName 直接进入 ORDER BY。
|
||||||
|
String orderColumn = memberCommonService.getSafeMineOrderColumn(pageForm.getPageOrderName());
|
||||||
|
String orderBy = PageUtil.getOrder(pageForm.getPageOrderBy());
|
||||||
|
if (StrUtil.isBlank(orderColumn) || StrUtil.isBlank(orderBy)) {
|
||||||
cnd.desc("info.applyDateTime");
|
cnd.desc("info.applyDateTime");
|
||||||
} else {
|
} else {
|
||||||
cnd.orderBy("info." + pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
|
cnd.orderBy(orderColumn, orderBy);
|
||||||
}
|
}
|
||||||
cnd.groupBy("t.id");
|
cnd.groupBy("t.id");
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
|
|||||||
@@ -115,4 +115,12 @@ public interface MemberCommonService extends BaseService<Sys_user> {
|
|||||||
* @param type 类型: apply or change
|
* @param type 类型: apply or change
|
||||||
*/
|
*/
|
||||||
void validateApplyOrChangeIsDoing(String userId, String type);
|
void validateApplyOrChangeIsDoing(String userId, String type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验会员申请、会员变更“我的申请”列表排序字段,并返回可用于 SQL 排序的安全字段名。
|
||||||
|
*
|
||||||
|
* @param pageOrderName 前端传入的排序字段
|
||||||
|
* @return 白名单内的 SQL 排序字段;不在白名单时返回空字符串
|
||||||
|
*/
|
||||||
|
String getSafeMineOrderColumn(String pageOrderName);
|
||||||
}
|
}
|
||||||
|
|||||||
+30
@@ -546,6 +546,36 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验会员申请、会员变更“我的申请”列表排序字段,避免前端参数直接拼接到 ORDER BY。
|
||||||
|
*
|
||||||
|
* @param pageOrderName 前端传入的排序字段
|
||||||
|
* @return 白名单内的 SQL 排序字段;不在白名单时返回空字符串
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getSafeMineOrderColumn(String pageOrderName) {
|
||||||
|
if (StrUtil.isBlank(pageOrderName)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅允许页面表格中存在的可排序字段参与 ORDER BY,未知字段交由 controller 回退默认排序。
|
||||||
|
return switch (pageOrderName) {
|
||||||
|
case "loginName" -> "info.loginName";
|
||||||
|
case "userName" -> "info.userName";
|
||||||
|
case "loginname" -> "info.loginname";
|
||||||
|
case "username" -> "info.username";
|
||||||
|
case "sex" -> "info.sex";
|
||||||
|
case "userState" -> "info.userState";
|
||||||
|
case "personType" -> "info.personType";
|
||||||
|
case "preparedBy" -> "info.preparedBy";
|
||||||
|
case "unionName" -> "info.unionName";
|
||||||
|
case "unitName" -> "info.unitName";
|
||||||
|
case "changeTypes" -> "info.changeTypes";
|
||||||
|
case "applyDateTime" -> "info.applyDateTime";
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private String booleanVerification(Object value) {
|
private String booleanVerification(Object value) {
|
||||||
// 检查是否为null或空字符串
|
// 检查是否为null或空字符串
|
||||||
|
|||||||
+25
@@ -42,6 +42,31 @@ public class WelfareProjectMangeController {
|
|||||||
@Inject
|
@Inject
|
||||||
private WelfareListService welfareListService;
|
private WelfareListService welfareListService;
|
||||||
|
|
||||||
|
/** projectId 为福利项目 ID;Result.data 为预计接收人数。 */
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("welfare.project.mange")
|
||||||
|
public Result reminderRecipientCount(@Param("projectId") String projectId) {
|
||||||
|
try {
|
||||||
|
return Result.success().addData(projectService.countReminderRecipients(projectId));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** projectId 为项目 ID,content 为提醒正文;成功仅表示已提交发送,data 为接收人数。 */
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("welfare.project.mange")
|
||||||
|
@SLog(tag = "福利", msg = "提交未选择人员提醒")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result submitSelectionReminder(@Param("projectId") String projectId, @Param("content") String content) {
|
||||||
|
try {
|
||||||
|
int count = projectService.submitSelectionReminder(projectId, content);
|
||||||
|
return Result.success("已提交发送,接收人数:" + count).addData(count);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("beetl:/platform/zhgh/welfare/projectMange/index.html")
|
@Ok("beetl:/platform/zhgh/welfare/projectMange/index.html")
|
||||||
@SaCheckPermission("welfare.project.mange")
|
@SaCheckPermission("welfare.project.mange")
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ public interface WelfareListService extends BaseService<WelfareList> {
|
|||||||
|
|
||||||
void doWelfareListUser(String[] ids,String id);
|
void doWelfareListUser(String[] ids,String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将单个用户加入福利项目名单,已存在时不重复添加。
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param projectId 福利项目ID
|
||||||
|
*/
|
||||||
|
void ensureWelfareListUser(String userId, String projectId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询
|
* 分页查询
|
||||||
* @param pageForm
|
* @param pageForm
|
||||||
|
|||||||
@@ -47,4 +47,10 @@ public interface WelfareProjectService extends BaseService<WelfareProject> {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<WelfareProject> getWelfareList(Integer year);
|
List<WelfareProject> getWelfareList(Integer year);
|
||||||
|
|
||||||
|
/** projectId 为福利项目 ID,返回名单内尚未选择且有工号的去重人数。 */
|
||||||
|
int countReminderRecipients(String projectId);
|
||||||
|
|
||||||
|
/** projectId 为福利项目 ID,content 为提醒正文;返回提交后台发送的人数。 */
|
||||||
|
int submitSelectionReminder(String projectId, String content);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import org.nutz.dao.Cnd;
|
|||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
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.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.json.Json;
|
import org.nutz.json.Json;
|
||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
@@ -381,6 +383,29 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
|
|||||||
insert(welfareLists);
|
insert(welfareLists);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void ensureWelfareListUser(String userId, String projectId) {
|
||||||
|
if (StrUtil.isBlank(userId) || StrUtil.isBlank(projectId)
|
||||||
|
|| count(Cnd.where("projectId", "=", projectId).and("userId", "=", userId)) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
View_user user = dao().fetch(View_user.class, userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new IllegalArgumentException("未找到当前用户信息,无法加入福利项目");
|
||||||
|
}
|
||||||
|
WelfareList welfareList = new WelfareList();
|
||||||
|
welfareList.setProjectId(projectId);
|
||||||
|
welfareList.setUserId(user.getId());
|
||||||
|
welfareList.setWelfareUnitId(user.getUnitId());
|
||||||
|
welfareList.setWelfareUnitName(user.getUnitName());
|
||||||
|
welfareList.setWelfareUnionId(user.getUnionId());
|
||||||
|
welfareList.setWelfareUnionName(user.getUnionName());
|
||||||
|
welfareList.setPersonType(user.getPersonType());
|
||||||
|
welfareList.setUserState(user.getUserState());
|
||||||
|
insert(welfareList);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pagination pageData(WelfareListPageForm pageForm) {
|
public Pagination pageData(WelfareListPageForm pageForm) {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package com.budwk.app.zhgh.welfare.service.impl;
|
package com.budwk.app.zhgh.welfare.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.thread.ThreadUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
|
import com.budwk.app.base.sms.SmsService;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.sys.models.Sys_home_activity;
|
import com.budwk.app.sys.models.Sys_home_activity;
|
||||||
import com.budwk.app.sys.models.Sys_user;
|
import com.budwk.app.sys.models.Sys_user;
|
||||||
import com.budwk.app.zhgh.welfare.model.WelfareList;
|
import com.budwk.app.zhgh.welfare.model.WelfareList;
|
||||||
@@ -15,6 +19,7 @@ import org.nutz.dao.Dao;
|
|||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
@@ -34,10 +39,70 @@ import java.util.stream.Collectors;
|
|||||||
public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> implements WelfareProjectService {
|
public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> implements WelfareProjectService {
|
||||||
private static final Logger log = LoggerFactory.getLogger(WelfareProjectServiceImpl.class);
|
private static final Logger log = LoggerFactory.getLogger(WelfareProjectServiceImpl.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SmsService smsService;
|
||||||
|
|
||||||
public WelfareProjectServiceImpl(Dao dao) {
|
public WelfareProjectServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按福利项目 ID 查询未选择人员,返回去重后的接收人数。 */
|
||||||
|
@Override
|
||||||
|
public int countReminderRecipients(String projectId) {
|
||||||
|
return listReminderRecipients(projectId).size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接收项目 ID 和提醒正文,返回后台任务接收人数,不代表送达结果。 */
|
||||||
|
@Override
|
||||||
|
public int submitSelectionReminder(String projectId, String content) {
|
||||||
|
if (StrUtil.isBlank(content)) {
|
||||||
|
throw new IllegalArgumentException("请输入提醒内容");
|
||||||
|
}
|
||||||
|
// 预览后可能有人完成选择,提交时重新筛选实际接收人。
|
||||||
|
List<String> recipients = listReminderRecipients(projectId);
|
||||||
|
if (recipients.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("该福利项目没有可提醒的未选择人员");
|
||||||
|
}
|
||||||
|
String sender = SecurityUtil.getUserLoginname();
|
||||||
|
if (StrUtil.isBlank(sender)) {
|
||||||
|
throw new IllegalArgumentException("无法获取发送人工号");
|
||||||
|
}
|
||||||
|
String url = "/platform/h5/welfare/userSelect/index?id=" + projectId;
|
||||||
|
String message = content.trim();
|
||||||
|
ThreadUtil.execute(() -> smsService.massSendTextCard(recipients, "福利选择提醒", message, null, url, sender));
|
||||||
|
return recipients.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回指定项目福利名单中没有有效选项记录的工号;空名单不扩大查询范围。
|
||||||
|
* 项目停用或选择截止时拒绝查询及提交,避免邀请进入无法选择的项目。
|
||||||
|
*/
|
||||||
|
private List<String> listReminderRecipients(String projectId) {
|
||||||
|
WelfareProject project = StrUtil.isBlank(projectId) ? null : fetch(projectId);
|
||||||
|
if (project == null) {
|
||||||
|
throw new IllegalArgumentException("福利项目不存在");
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(project.getIsDisabled()) || project.getChoiceTimeEnd() == null
|
||||||
|
|| !new Date().before(project.getChoiceTimeEnd())) {
|
||||||
|
throw new IllegalArgumentException("福利项目已停用或选择已截止,无法发送提醒");
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT DISTINCT u.loginname
|
||||||
|
FROM welfare_list wl
|
||||||
|
JOIN sys_user u ON u.id = wl.userId
|
||||||
|
WHERE wl.projectId = @projectId
|
||||||
|
AND u.loginname IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM welfare_project_user_selection s
|
||||||
|
WHERE s.welfareId = wl.projectId AND s.selectUserId = wl.userId
|
||||||
|
AND s.selectOptionId IS NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
sql.setParam("projectId", projectId);
|
||||||
|
return listMap(sql).stream().map(row -> row.getString("loginname"))
|
||||||
|
.filter(StrUtil::isNotBlank).map(String::trim).distinct().toList();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WelfareProject projectInfo(String projectId) {
|
public WelfareProject projectInfo(String projectId) {
|
||||||
WelfareProject project = fetch(projectId);
|
WelfareProject project = fetch(projectId);
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
-- 问卷服务菜单初始化脚本。
|
||||||
|
-- 使用 permission 做幂等判断,避免同一环境重复执行时插入重复菜单。
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT
|
||||||
|
'8f3d9c78a9f04b5b8b7f0f6a00010001',
|
||||||
|
'',
|
||||||
|
LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
|
||||||
|
'问卷服务',
|
||||||
|
'Qsv',
|
||||||
|
'menu',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'ti-check-box',
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
'qsv',
|
||||||
|
NULL,
|
||||||
|
992,
|
||||||
|
1,
|
||||||
|
'',
|
||||||
|
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||||
|
'',
|
||||||
|
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||||
|
0,
|
||||||
|
'PC',
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
'w',
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
FROM sys_menu
|
||||||
|
WHERE (parentId = '' OR parentId IS NULL)
|
||||||
|
AND CHAR_LENGTH(path) = 4
|
||||||
|
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv') t);
|
||||||
|
|
||||||
|
-- 新建问卷入口,对应 /platform/qsv/new。
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT '8f3d9c78a9f04b5b8b7f0f6a00010002', p.id, CONCAT(p.path, '0001'), '新建问卷', 'Qsv New', 'menu', '/platform/qsv/new', 'data-pjax', '', 1, 0, 'qsv.new', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.new') t);
|
||||||
|
|
||||||
|
-- 问卷活动管理入口,对应 /platform/qsv/activity。
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT '8f3d9c78a9f04b5b8b7f0f6a00010003', p.id, CONCAT(p.path, '0002'), '问卷管理', 'Qsv Activity', 'menu', '/platform/qsv/activity', 'data-pjax', '', 1, 0, 'qsv.activity', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'w', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.activity') t);
|
||||||
|
|
||||||
|
-- 在线答题管理入口,对应 /platform/qsv/online。
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT '8f3d9c78a9f04b5b8b7f0f6a00010004', p.id, CONCAT(p.path, '0003'), '在线答题', 'Qsv Online', 'menu', '/platform/qsv/online', 'data-pjax', '', 1, 0, 'qsv.online', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'z', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.online') t);
|
||||||
|
|
||||||
|
-- 问卷统计入口,对应 /platform/qsv/survey。
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT '8f3d9c78a9f04b5b8b7f0f6a00010005', p.id, CONCAT(p.path, '0004'), '问卷统计', 'Qsv Survey', 'menu', '/platform/qsv/survey', 'data-pjax', '', 1, 0, 'qsv.survey', NULL, 4, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'w', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.survey') t);
|
||||||
|
|
||||||
|
-- 答题排名入口,对应 /platform/qsv/quizRank。
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT '8f3d9c78a9f04b5b8b7f0f6a00010006', p.id, CONCAT(p.path, '0005'), '答题排名', 'Qsv Quiz Rank', 'menu', '/platform/qsv/quizRank', 'data-pjax', '', 1, 0, 'qsv.quiz.rank', NULL, 5, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'd', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.quiz.rank') t);
|
||||||
|
|
||||||
|
-- 题库管理入口,对应 /platform/qsv/bank。
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT '8f3d9c78a9f04b5b8b7f0f6a00010007', p.id, CONCAT(p.path, '0006'), '题库管理', 'Qsv Bank', 'menu', '/platform/qsv/bank', 'data-pjax', '', 1, 0, 'qsv.bank', NULL, 6, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 't', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.bank') t);
|
||||||
|
|
||||||
|
-- 答题记录删除按钮权限,对应 qsv.quiz.rank.delete。
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT '8f3d9c78a9f04b5b8b7f0f6a00010008', p.id, CONCAT(p.path, '0001'), '删除答题记录', 'Delete Quiz Record', 'data', '', '', '', 0, 0, 'qsv.quiz.rank.delete', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 's', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
WHERE p.permission = 'qsv.quiz.rank'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.quiz.rank.delete') t);
|
||||||
|
|
||||||
|
-- 移动端问卷服务入口,对应 /platform/h5/qsv。
|
||||||
|
-- @SaCheckPermission 手机端权限需要使用 h5. 前缀。
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT
|
||||||
|
'8f3d9c78a9f04b5b8b7f0f6a00011001',
|
||||||
|
'',
|
||||||
|
LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
|
||||||
|
'问卷服务',
|
||||||
|
'Qsv H5',
|
||||||
|
'menu',
|
||||||
|
'/platform/h5/qsv',
|
||||||
|
'data-pjax',
|
||||||
|
'records-o',
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
'h5.qsv',
|
||||||
|
NULL,
|
||||||
|
992,
|
||||||
|
0,
|
||||||
|
'',
|
||||||
|
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||||
|
'',
|
||||||
|
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||||
|
0,
|
||||||
|
'H5',
|
||||||
|
NULL,
|
||||||
|
'records-o',
|
||||||
|
'w',
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
FROM sys_menu
|
||||||
|
WHERE (parentId = '' OR parentId IS NULL)
|
||||||
|
AND CHAR_LENGTH(path) = 4
|
||||||
|
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'h5.qsv') t);
|
||||||
|
|
||||||
|
-- 默认给系统管理员授权问卷服务所有菜单与按钮权限。
|
||||||
|
INSERT INTO sys_role_menu (roleId, menuId)
|
||||||
|
SELECT r.id, m.id
|
||||||
|
FROM sys_role r
|
||||||
|
JOIN sys_menu m ON m.permission IN (
|
||||||
|
'qsv',
|
||||||
|
'qsv.new',
|
||||||
|
'qsv.activity',
|
||||||
|
'qsv.online',
|
||||||
|
'qsv.survey',
|
||||||
|
'qsv.quiz.rank',
|
||||||
|
'qsv.quiz.rank.delete',
|
||||||
|
'qsv.bank',
|
||||||
|
'h5.qsv'
|
||||||
|
)
|
||||||
|
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
|
||||||
|
WHERE r.code = 'SYSADMIN'
|
||||||
|
AND rm.roleId IS NULL;
|
||||||
@@ -136,7 +136,7 @@ JOIN sys_menu m ON m.permission IN (
|
|||||||
'tour.undertake'
|
'tour.undertake'
|
||||||
)
|
)
|
||||||
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
|
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
|
||||||
WHERE r.code = 'BRANCH_UNION_CHAIRMAN'
|
WHERE r.code IN ('BRANCH_UNION_CHAIRMAN', 'TOUR_APPLICANT')
|
||||||
AND rm.roleId IS NULL;
|
AND rm.roleId IS NULL;
|
||||||
|
|
||||||
-- 普惠疗休养移动端菜单。
|
-- 普惠疗休养移动端菜单。
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `qsv_subject`
|
||||||
|
ADD COLUMN `componentType` VARCHAR(30) NULL COMMENT '题目组件展示类型(RADIO普通单选,DROPDOWN下拉选择)' AFTER `type`;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- 疗休养台账增加成团信息字段,用于线路成团自动分配和人员调整。
|
||||||
|
ALTER TABLE `tour_ledger`
|
||||||
|
ADD COLUMN `groupCode` varchar(20) DEFAULT NULL COMMENT '成团编码' AFTER `lineType`,
|
||||||
|
ADD COLUMN `groupName` varchar(50) DEFAULT NULL COMMENT '成团名称' AFTER `groupCode`;
|
||||||
|
|
||||||
|
ALTER TABLE `tour_ledger`
|
||||||
|
ADD KEY `idx_tour_ledger_matter_line_group` (`matterId`, `lineId`, `groupCode`, `delFlag`);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE tour_ledger
|
||||||
|
ADD COLUMN undertakeUnionId VARCHAR(32) NULL COMMENT '领办工会编码' AFTER groupName,
|
||||||
|
ADD COLUMN undertakeUnionName VARCHAR(100) NULL COMMENT '领办工会' AFTER undertakeUnionId;
|
||||||
|
|
||||||
|
CREATE INDEX idx_tour_ledger_undertake_union ON tour_ledger (matterId, groupCode, undertakeUnionId);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Convert tour line sort code to numeric type for stable numeric ascending order.
|
||||||
|
UPDATE tour_line
|
||||||
|
SET lineCode = '0'
|
||||||
|
WHERE lineCode IS NULL
|
||||||
|
OR TRIM(lineCode) = ''
|
||||||
|
OR lineCode NOT REGEXP '^[0-9]+$';
|
||||||
|
|
||||||
|
ALTER TABLE tour_line
|
||||||
|
MODIFY COLUMN lineCode INT(11) NOT NULL DEFAULT 0 COMMENT '排序编号';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- 疗休养事项选择线路新增最多报名人数,用于报名提交容量控制;为空或0表示不限制。
|
||||||
|
ALTER TABLE `tour_matter`
|
||||||
|
ADD COLUMN `maxSignupPeople` int DEFAULT NULL COMMENT '最多报名人数' AFTER `maxGroupPeople`;
|
||||||
@@ -10,6 +10,17 @@ CREATE TABLE IF NOT EXISTS `tour_matter` (
|
|||||||
`undertakeStatus` varchar(20) DEFAULT 'unclaimed' COMMENT '领办状态 claimed已领 unclaimed未领',
|
`undertakeStatus` varchar(20) DEFAULT 'unclaimed' COMMENT '领办状态 claimed已领 unclaimed未领',
|
||||||
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
|
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
|
||||||
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
|
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
|
||||||
|
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
|
||||||
|
`signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间',
|
||||||
|
`signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间',
|
||||||
|
`travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始时间',
|
||||||
|
`travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束时间',
|
||||||
|
`contactName` varchar(30) DEFAULT NULL COMMENT '联系人',
|
||||||
|
`contactPhone` varchar(30) DEFAULT NULL COMMENT '联系方式',
|
||||||
|
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
|
||||||
|
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
|
||||||
|
`maxSignupPeople` int DEFAULT NULL COMMENT '最多报名人数',
|
||||||
|
`estimatedCost` decimal(10,2) DEFAULT NULL COMMENT '预计费用',
|
||||||
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
|
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
|
||||||
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
|
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||||
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
|
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
|
||||||
@@ -22,5 +33,6 @@ CREATE TABLE IF NOT EXISTS `tour_matter` (
|
|||||||
KEY `idx_tour_matter_creator` (`creatorUserId`),
|
KEY `idx_tour_matter_creator` (`creatorUserId`),
|
||||||
KEY `idx_tour_matter_setting` (`settingId`),
|
KEY `idx_tour_matter_setting` (`settingId`),
|
||||||
KEY `idx_tour_matter_union` (`unionId`),
|
KEY `idx_tour_matter_union` (`unionId`),
|
||||||
KEY `idx_tour_matter_org_type` (`organizationType`)
|
KEY `idx_tour_matter_org_type` (`organizationType`),
|
||||||
|
KEY `idx_tour_matter_line` (`lineId`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项';
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `tour_participant_roster`
|
||||||
|
(
|
||||||
|
`id` varchar(32) NOT NULL COMMENT 'ID',
|
||||||
|
`settingId` varchar(32) NOT NULL COMMENT '疗休养配置ID',
|
||||||
|
`jobNo` varchar(50) NOT NULL COMMENT '工号',
|
||||||
|
`userName` varchar(100) DEFAULT NULL COMMENT '姓名',
|
||||||
|
`unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID',
|
||||||
|
`unitName` varchar(100) DEFAULT NULL COMMENT '所在单位',
|
||||||
|
`unionId` varchar(32) DEFAULT NULL COMMENT '所在工会ID',
|
||||||
|
`unionName` varchar(100) DEFAULT NULL COMMENT '所在工会',
|
||||||
|
`joined` tinyint(1) DEFAULT 0 COMMENT '是否参加:0否,1是',
|
||||||
|
`ledgerId` varchar(32) DEFAULT NULL COMMENT '疗休养台账ID',
|
||||||
|
`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_tour_participant_roster_setting_job` (`settingId`, `jobNo`),
|
||||||
|
KEY `idx_tour_participant_roster_setting` (`settingId`),
|
||||||
|
KEY `idx_tour_participant_roster_job` (`jobNo`),
|
||||||
|
KEY `idx_tour_participant_roster_joined` (`joined`),
|
||||||
|
KEY `idx_tour_participant_roster_ledger` (`ledgerId`),
|
||||||
|
KEY `idx_tour_participant_roster_union` (`unionId`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4 COMMENT ='疗休养人员名单';
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- 疗休养人员名单查询菜单。
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10014', p.id, CONCAT(p.path, '0013'), '疗休养人员名单', 'Tour Participant Roster', 'menu', '/platform/tour/participantRoster', 'data-pjax', '', 1, 0, 'tour.participantRoster', NULL, 13, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
WHERE p.permission = 'tour'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.participantRoster') t);
|
||||||
|
|
||||||
|
-- 默认给系统管理员授权,其它角色可以在角色管理中按需分配。
|
||||||
|
INSERT INTO sys_role_menu (roleId, menuId)
|
||||||
|
SELECT r.id, m.id
|
||||||
|
FROM sys_role r
|
||||||
|
JOIN sys_menu m ON m.permission = 'tour.participantRoster'
|
||||||
|
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
|
||||||
|
WHERE r.code = 'SYSADMIN'
|
||||||
|
AND rm.roleId IS NULL;
|
||||||
@@ -121,6 +121,17 @@ CREATE TABLE IF NOT EXISTS `tour_matter` (
|
|||||||
`undertakeStatus` varchar(20) DEFAULT 'unclaimed' COMMENT '领办状态 claimed已领 unclaimed未领',
|
`undertakeStatus` varchar(20) DEFAULT 'unclaimed' COMMENT '领办状态 claimed已领 unclaimed未领',
|
||||||
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
|
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
|
||||||
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
|
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
|
||||||
|
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
|
||||||
|
`signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间',
|
||||||
|
`signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间',
|
||||||
|
`travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始时间',
|
||||||
|
`travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束时间',
|
||||||
|
`contactName` varchar(30) DEFAULT NULL COMMENT '联系人',
|
||||||
|
`contactPhone` varchar(30) DEFAULT NULL COMMENT '联系方式',
|
||||||
|
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
|
||||||
|
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
|
||||||
|
`maxSignupPeople` int DEFAULT NULL COMMENT '最多报名人数',
|
||||||
|
`estimatedCost` decimal(10,2) DEFAULT NULL COMMENT '预计费用',
|
||||||
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
|
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
|
||||||
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
|
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||||
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
|
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
|
||||||
@@ -133,7 +144,8 @@ CREATE TABLE IF NOT EXISTS `tour_matter` (
|
|||||||
KEY `idx_tour_matter_creator` (`creatorUserId`),
|
KEY `idx_tour_matter_creator` (`creatorUserId`),
|
||||||
KEY `idx_tour_matter_setting` (`settingId`),
|
KEY `idx_tour_matter_setting` (`settingId`),
|
||||||
KEY `idx_tour_matter_union` (`unionId`),
|
KEY `idx_tour_matter_union` (`unionId`),
|
||||||
KEY `idx_tour_matter_org_type` (`organizationType`)
|
KEY `idx_tour_matter_org_type` (`organizationType`),
|
||||||
|
KEY `idx_tour_matter_line` (`lineId`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项';
|
||||||
|
|
||||||
-- 疗休养事项批次表。
|
-- 疗休养事项批次表。
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ JOIN sys_menu m ON m.permission IN (
|
|||||||
'tour.undertake'
|
'tour.undertake'
|
||||||
)
|
)
|
||||||
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
|
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
|
||||||
WHERE r.code IN ('SYSADMIN', 'BRANCH_UNION_CHAIRMAN')
|
WHERE r.code IN ('SYSADMIN', 'BRANCH_UNION_CHAIRMAN', 'TOUR_APPLICANT')
|
||||||
AND rm.roleId IS NULL;
|
AND rm.roleId IS NULL;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
const owners = {}
|
||||||
|
const stack = []
|
||||||
|
const popupHandledKey = "__mobilePopupHandled"
|
||||||
|
let sequence = 0
|
||||||
|
|
||||||
|
function pushHistory(ownerId, popupId) {
|
||||||
|
const token = "popup-" + (++sequence)
|
||||||
|
stack.push({ownerId: ownerId, popupId: popupId, token: token})
|
||||||
|
// 保留当前 PJAX 状态,避免关闭弹框回退时被识别为页面切换并重新加载外层页面。
|
||||||
|
const currentState = window.history.state || {}
|
||||||
|
const popupState = Object.assign({}, currentState, {mobilePopupToken: token})
|
||||||
|
window.history.pushState(popupState, "", window.location.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTopFromHistory() {
|
||||||
|
const item = stack.pop()
|
||||||
|
if (!item) return false
|
||||||
|
const callback = owners[item.ownerId]
|
||||||
|
if (callback) callback(item.popupId)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("popstate", (event) => {
|
||||||
|
if (closeTopFromHistory()) {
|
||||||
|
// 标记本次回退已由弹框处理,供页面中的 PJAX 监听跳过重复处理。
|
||||||
|
event[popupHandledKey] = true
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
window.mobilePopupHistory = {
|
||||||
|
register(ownerId, closeCallback) {
|
||||||
|
owners[ownerId] = closeCallback
|
||||||
|
},
|
||||||
|
unregister(ownerId) {
|
||||||
|
delete owners[ownerId]
|
||||||
|
for (let i = stack.length - 1; i >= 0; i--) {
|
||||||
|
if (stack[i].ownerId === ownerId) stack.splice(i, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
open(ownerId, popupId) {
|
||||||
|
if (!stack.some((item) => item.ownerId === ownerId && item.popupId === popupId)) {
|
||||||
|
pushHistory(ownerId, popupId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close(ownerId, popupId) {
|
||||||
|
const top = stack[stack.length - 1]
|
||||||
|
if (top && top.ownerId === ownerId && top.popupId === popupId) {
|
||||||
|
window.history.back()
|
||||||
|
} else if (owners[ownerId]) {
|
||||||
|
owners[ownerId](popupId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear(ownerId) {
|
||||||
|
for (let i = stack.length - 1; i >= 0; i--) {
|
||||||
|
if (stack[i].ownerId === ownerId) stack.splice(i, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasOpen(ownerId) {
|
||||||
|
return stack.some((item) => item.ownerId === ownerId)
|
||||||
|
},
|
||||||
|
isHandledPopstate(event) {
|
||||||
|
return Boolean(event && event[popupHandledKey])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
const tourTableHeightMixins = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tourTableHeight: Math.max(120, window.innerHeight - 378),
|
||||||
|
tourTableResizeTimer: null,
|
||||||
|
tourTableHeightReady: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateTourTableHeight() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const table = this.$refs.tourMainTable || this.$refs.tableRef || this.$refs.table
|
||||||
|
const tableEl = table && table.$el ? table.$el : null
|
||||||
|
if (!tableEl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据当前屏幕、表格位置、分页区域动态计算主列表高度,避免整页被数据行撑出滚动条。
|
||||||
|
const mainContent = document.querySelector(".sub-app-container-main-content")
|
||||||
|
const viewportBottom = mainContent ? mainContent.getBoundingClientRect().bottom : window.innerHeight
|
||||||
|
const tableTop = tableEl.getBoundingClientRect().top
|
||||||
|
const pagination = this.$el.querySelector(".el-pagination-container")
|
||||||
|
const paginationHeight = pagination ? pagination.getBoundingClientRect().height : 46
|
||||||
|
const reserveHeight = Number(this.tourTableReserveHeight || 42)
|
||||||
|
const minHeight = Number(this.tourTableMinHeight || 120)
|
||||||
|
const height = Math.floor(viewportBottom - tableTop - paginationHeight - reserveHeight)
|
||||||
|
this.tourTableHeight = height > minHeight ? height : minHeight
|
||||||
|
this.tourTableHeightReady = true
|
||||||
|
|
||||||
|
// Element 表格高度变化后需要重新布局,否则固定列和表头可能错位。
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (table && table.doLayout) {
|
||||||
|
table.doLayout()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleTourTableResize() {
|
||||||
|
if (this.tourTableResizeTimer) {
|
||||||
|
clearTimeout(this.tourTableResizeTimer)
|
||||||
|
}
|
||||||
|
this.tourTableResizeTimer = setTimeout(() => {
|
||||||
|
this.updateTourTableHeight()
|
||||||
|
this.tourTableResizeTimer = null
|
||||||
|
}, 120)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.updateTourTableHeight()
|
||||||
|
window.addEventListener("resize", this.handleTourTableResize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.tourTableResizeTimer) {
|
||||||
|
clearTimeout(this.tourTableResizeTimer)
|
||||||
|
this.tourTableResizeTimer = null
|
||||||
|
}
|
||||||
|
window.removeEventListener("resize", this.handleTourTableResize)
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
tableData() {
|
||||||
|
this.updateTourTableHeight()
|
||||||
|
},
|
||||||
|
"pageForm.pageSize"() {
|
||||||
|
this.updateTourTableHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-item" v-if="is_sysadmin || is_A06">
|
<div class="search-item" v-if="has_school_scope_view_permission">
|
||||||
<div class="search-item-label">所属工会</div>
|
<div class="search-item-label">所属工会</div>
|
||||||
<div class="search-item-option">
|
<div class="search-item-option">
|
||||||
<el-select
|
<el-select
|
||||||
@@ -196,6 +196,12 @@ module.exports = {
|
|||||||
is_A06() {
|
is_A06() {
|
||||||
return this.roleData.is_A06
|
return this.roleData.is_A06
|
||||||
},
|
},
|
||||||
|
is_school_union_activity_admin() {
|
||||||
|
return this.roleData.is_school_union_activity_admin
|
||||||
|
},
|
||||||
|
has_school_scope_view_permission() {
|
||||||
|
return this.is_sysadmin || this.is_A06 || this.is_school_union_activity_admin
|
||||||
|
},
|
||||||
is_H04() {
|
is_H04() {
|
||||||
return this.roleData.is_H04
|
return this.roleData.is_H04
|
||||||
},
|
},
|
||||||
@@ -292,7 +298,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
async flushUnits() {
|
async flushUnits() {
|
||||||
this.$set(this.pageForm, "unitId", "")
|
this.$set(this.pageForm, "unitId", "")
|
||||||
if (this.is_A06 || this.is_sysadmin) {
|
if (this.has_school_scope_view_permission) {
|
||||||
this.units = await this.$businessTool.listUnit(this.pageForm.unionId)
|
this.units = await this.$businessTool.listUnit(this.pageForm.unionId)
|
||||||
// this.ActivityUnits = await getActivityUnits(this.pageForm.activityUnionId)
|
// this.ActivityUnits = await getActivityUnits(this.pageForm.activityUnionId)
|
||||||
} else {
|
} else {
|
||||||
@@ -328,11 +334,11 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
|
await this.getRolesAndUnion()
|
||||||
this.unions = await this.$businessTool.listUnion()
|
this.unions = await this.$businessTool.listUnion()
|
||||||
// this.ActivityUnions = await getActivityUnions()
|
// this.ActivityUnions = await getActivityUnions()
|
||||||
await this.flushUnits()
|
await this.flushUnits()
|
||||||
await this.getActivityGroup()
|
await this.getActivityGroup()
|
||||||
await this.getRolesAndUnion()
|
|
||||||
this.viewGroupName()
|
this.viewGroupName()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row class="query-row" v-if="is_A06 === true || is_sysadmin === true || is_H04 === true">
|
<el-row class="query-row" v-if="has_school_scope_view_permission === true || is_H04 === true">
|
||||||
<el-col class="query-title hidden-xs-only">会员类型:</el-col>
|
<el-col class="query-title hidden-xs-only">会员类型:</el-col>
|
||||||
<el-col class="query-content">
|
<el-col class="query-content">
|
||||||
<el-tag
|
<el-tag
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row class="query-row" v-if="is_A06 === true || is_sysadmin === true || is_H04 === true">
|
<el-row class="query-row" v-if="has_school_scope_view_permission === true || is_H04 === true">
|
||||||
<el-col class="query-title">教职工类别:</el-col>
|
<el-col class="query-title">教职工类别:</el-col>
|
||||||
<el-col class="query-content">
|
<el-col class="query-content">
|
||||||
<el-tag
|
<el-tag
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row class="query-row" v-if="is_A06 === true || is_sysadmin === true || is_H04 === true">
|
<el-row class="query-row" v-if="has_school_scope_view_permission === true || is_H04 === true">
|
||||||
<el-col class="query-title">在职状态:</el-col>
|
<el-col class="query-title">在职状态:</el-col>
|
||||||
<el-col class="query-content">
|
<el-col class="query-content">
|
||||||
<el-tag
|
<el-tag
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row class="query-row" v-if="is_A06 === true || is_sysadmin === true || is_H04 === true">
|
<el-row class="query-row" v-if="has_school_scope_view_permission === true || is_H04 === true">
|
||||||
<el-col class="query-title">部门查询:</el-col>
|
<el-col class="query-title">部门查询:</el-col>
|
||||||
<el-col class="query-content">
|
<el-col class="query-content">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
flushUnits()
|
flushUnits()
|
||||||
doSearch()
|
doSearch()
|
||||||
"
|
"
|
||||||
:clearable="is_A06 === true || is_sysadmin === true"
|
:clearable="has_school_scope_view_permission === true"
|
||||||
filterable
|
filterable
|
||||||
placeholder="所属工会"
|
placeholder="所属工会"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<template v-if="is_A06 === true || is_sysadmin === true">
|
<template v-if="has_school_scope_view_permission === true">
|
||||||
<!--<el-row class="query-row">
|
<!--<el-row class="query-row">
|
||||||
<el-col class="query-title">活动部门:</el-col>
|
<el-col class="query-title">活动部门:</el-col>
|
||||||
<el-col class="query-content">
|
<el-col class="query-content">
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row class="query-row" v-if="is_A06 === true || is_sysadmin === true || is_H04 === true">
|
<el-row class="query-row" v-if="has_school_scope_view_permission === true || is_H04 === true">
|
||||||
<el-col class="query-title">年龄范围:</el-col>
|
<el-col class="query-title">年龄范围:</el-col>
|
||||||
<el-col class="query-content">
|
<el-col class="query-content">
|
||||||
<el-row type="flex" style="align-items: center">
|
<el-row type="flex" style="align-items: center">
|
||||||
@@ -476,6 +476,12 @@ module.exports = {
|
|||||||
is_A06() {
|
is_A06() {
|
||||||
return this.roleData.is_A06
|
return this.roleData.is_A06
|
||||||
},
|
},
|
||||||
|
is_school_union_activity_admin() {
|
||||||
|
return this.roleData.is_school_union_activity_admin
|
||||||
|
},
|
||||||
|
has_school_scope_view_permission() {
|
||||||
|
return this.is_sysadmin || this.is_A06 || this.is_school_union_activity_admin
|
||||||
|
},
|
||||||
is_H04() {
|
is_H04() {
|
||||||
return this.roleData.is_H04
|
return this.roleData.is_H04
|
||||||
},
|
},
|
||||||
@@ -561,7 +567,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
async flushUnits() {
|
async flushUnits() {
|
||||||
this.$set(this.pageForm, "unitId", "")
|
this.$set(this.pageForm, "unitId", "")
|
||||||
if (this.is_sysadmin || this.is_A06) {
|
if (this.has_school_scope_view_permission) {
|
||||||
this.units = await this.$businessTool.listUnit(this.pageForm.unionId)
|
this.units = await this.$businessTool.listUnit(this.pageForm.unionId)
|
||||||
// this.activityUnits = await getActivityUnits(this.pageForm.activityUnionId)
|
// this.activityUnits = await getActivityUnits(this.pageForm.activityUnionId)
|
||||||
} else {
|
} else {
|
||||||
@@ -662,7 +668,7 @@ module.exports = {
|
|||||||
this.listSession()
|
this.listSession()
|
||||||
await this.getRolesAndUnion()
|
await this.getRolesAndUnion()
|
||||||
this.clubOptions = await this.$businessTool.listCLubByRole()
|
this.clubOptions = await this.$businessTool.listCLubByRole()
|
||||||
if ((this.is_sysadmin || this.is_A06 || this.is_H02) === false && this.is_H04 === true) {
|
if ((this.has_school_scope_view_permission || this.is_H02) === false && this.is_H04 === true) {
|
||||||
this.unions = this.$businessTool.listUnion(this.unionid)
|
this.unions = this.$businessTool.listUnion(this.unionid)
|
||||||
this.$set(this.pageForm, "unionId", this.unions[0].id)
|
this.$set(this.pageForm, "unionId", this.unions[0].id)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<van-form ref="form">
|
||||||
|
<template v-for="column in dynamicData">
|
||||||
|
<van-field
|
||||||
|
v-if="column.columnFormType === 'STEPPER'"
|
||||||
|
:key="column.columnCode"
|
||||||
|
:label="column.columnName"
|
||||||
|
:required="column.isRequired"
|
||||||
|
>
|
||||||
|
<template #input>
|
||||||
|
<van-stepper v-model="column.columnValue" :min="1"></van-stepper>
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
<van-field
|
||||||
|
v-else-if="column.columnFormType === 'SELECT' && ['DATE', 'DATETIME'].includes(column.columnType)"
|
||||||
|
:key="column.columnCode"
|
||||||
|
:label="column.columnName"
|
||||||
|
:required="column.isRequired"
|
||||||
|
:rules="[{required: column.isRequired, message: '请选择' + column.columnName}]"
|
||||||
|
>
|
||||||
|
<template #input>
|
||||||
|
<input
|
||||||
|
v-model="column.columnValue"
|
||||||
|
:type="column.columnType === 'DATE' ? 'date' : 'datetime-local'"
|
||||||
|
class="native-input"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
<van-field
|
||||||
|
v-else-if="column.columnFormType !== 'SELECT' && column.columnType !== 'JSON'"
|
||||||
|
:key="column.columnCode"
|
||||||
|
v-model="column.columnValue"
|
||||||
|
:label="column.columnName"
|
||||||
|
:name="column.columnCode"
|
||||||
|
:required="column.isRequired"
|
||||||
|
:type="column.columnType === 'INT' ? 'digit' : 'text'"
|
||||||
|
:placeholder="'请填写' + column.columnName"
|
||||||
|
:rules="[{required: column.isRequired, message: '请填写' + column.columnName}]"
|
||||||
|
></van-field>
|
||||||
|
<van-field
|
||||||
|
v-else-if="column.columnFormType === 'SELECT'"
|
||||||
|
:key="column.columnCode"
|
||||||
|
:label="column.columnName"
|
||||||
|
:required="column.isRequired"
|
||||||
|
:rules="[{required: column.isRequired, message: '请选择' + column.columnName}]"
|
||||||
|
>
|
||||||
|
<template #input>
|
||||||
|
<select v-model="column.columnValue" class="native-select">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option v-for="option in column.selectValues" :key="option" :value="option">{{option}}</option>
|
||||||
|
</select>
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
<van-field
|
||||||
|
v-else
|
||||||
|
:key="column.columnCode"
|
||||||
|
:label="column.columnName"
|
||||||
|
:required="column.isRequired"
|
||||||
|
:rules="[{required: column.isRequired, message: '请上传' + column.columnName}]"
|
||||||
|
>
|
||||||
|
<template #input>
|
||||||
|
<h5-file-upload
|
||||||
|
:value.sync="column.columnValue"
|
||||||
|
:upload_number="column.fileNumber"
|
||||||
|
upload_mode="file"
|
||||||
|
upload_result_category="array"
|
||||||
|
upload_result_type="url"
|
||||||
|
complete_result
|
||||||
|
></h5-file-upload>
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
</template>
|
||||||
|
</van-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
module.exports = {
|
||||||
|
name: "TrainSignDynamicForm",
|
||||||
|
props: {
|
||||||
|
dynamicData: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
validate() {
|
||||||
|
return this.$refs.form.validate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.native-select {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
|
.native-input {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="position: relative !important">
|
<div style="position: relative !important">
|
||||||
<div style="margin-bottom: 10px">
|
<!-- 点位搜索暂时隐藏,保留现有实现便于后续恢复。 -->
|
||||||
|
<div v-if="false" style="margin-bottom: 10px">
|
||||||
<el-autocomplete
|
<el-autocomplete
|
||||||
v-model="addressValue"
|
v-model="addressValue"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
|||||||
@@ -21,37 +21,63 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
content: {
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
// 详情数据通常在组件创建后异步回填,每次内容变化都重新识别并渲染 PDF。
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
pdf: true,
|
pdf: true,
|
||||||
pdfObj: null,
|
pdfObj: null,
|
||||||
|
previewVersion: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
init() {
|
||||||
|
this.destroyPdf()
|
||||||
|
const currentVersion = ++this.previewVersion
|
||||||
try {
|
try {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser()
|
||||||
const doc = parser.parseFromString(this.content, 'text/html');
|
const doc = parser.parseFromString(this.content || '', 'text/html')
|
||||||
const link = doc.querySelector('a');
|
const link = doc.querySelector('a')
|
||||||
const href = link.getAttribute('href');
|
const href = link ? link.getAttribute('href') : ''
|
||||||
if (!href) {
|
if (!href) {
|
||||||
this.pdf = false
|
this.pdf = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parsedUrl = new URL(href, window.location.origin)
|
||||||
|
const linkText = (link.textContent || '').trim().toLowerCase()
|
||||||
|
const isPdf = linkText.endsWith('.pdf') || parsedUrl.pathname.toLowerCase().endsWith('.pdf')
|
||||||
|
if (!isPdf) {
|
||||||
|
this.pdf = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统上传文件可能保存了其他环境的绝对域名,内部下载地址统一使用当前站点。
|
||||||
|
const pdfUrl = parsedUrl.pathname === '/platform/sys/file/download'
|
||||||
|
? parsedUrl.pathname + parsedUrl.search : parsedUrl.href
|
||||||
|
this.pdf = true
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
if (currentVersion !== this.previewVersion || !this.pdf) return
|
||||||
this.pdfObj = new Pdfh5('#pdf-container', {
|
this.pdfObj = new Pdfh5('#pdf-container', {
|
||||||
pdfurl: href,
|
pdfurl: pdfUrl,
|
||||||
});
|
// 使用二进制数据直接交给 PDF.js,避免 Blob 转 Base64 后被 CSP 的 connect-src 拦截。
|
||||||
this.pdfObj.on("complete", function () {
|
responseType: 'arraybuffer',
|
||||||
const elements = document.querySelectorAll('[class*="canvasImg"]')
|
})
|
||||||
let classArray = []
|
this.pdfObj.on("complete", () => {
|
||||||
|
if (currentVersion !== this.previewVersion) return
|
||||||
|
const elements = this.$el.querySelectorAll('[class*="canvasImg"]')
|
||||||
|
const classArray = []
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
classArray.push(element.getAttribute('src'))
|
classArray.push(element.getAttribute('src'))
|
||||||
})
|
})
|
||||||
elements.forEach((element,index) => {
|
elements.forEach((element,index) => {
|
||||||
element.addEventListener('click', function() {
|
element.addEventListener('click', () => {
|
||||||
vant.ImagePreview({
|
vant.ImagePreview({
|
||||||
images: classArray,
|
images: classArray,
|
||||||
startPosition: index,
|
startPosition: index,
|
||||||
@@ -65,9 +91,17 @@ module.exports = {
|
|||||||
this.pdf = false
|
this.pdf = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
destroyPdf() {
|
||||||
|
// 内容切换或组件销毁时释放 PDF 渲染实例,避免重复打开详情产生资源残留。
|
||||||
|
if (this.pdfObj && this.pdfObj.destroy) {
|
||||||
|
this.pdfObj.destroy()
|
||||||
|
}
|
||||||
|
this.pdfObj = null
|
||||||
},
|
},
|
||||||
created() {
|
},
|
||||||
this.init()
|
beforeDestroy() {
|
||||||
|
this.previewVersion += 1
|
||||||
|
this.destroyPdf()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,74 +1,128 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div ref="editorRef"></div>
|
<div ref="editorRef"></div>
|
||||||
<input type="file" style="display: none" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const { $, BtnMenu, Panel, Tooltip } = wangEditor
|
const { $, BtnMenu, Panel, Tooltip } = wangEditor
|
||||||
|
|
||||||
|
const showImportLoading = () => {
|
||||||
|
if (window.ELEMENT) {
|
||||||
|
return ELEMENT.Loading.service({
|
||||||
|
lock: true,
|
||||||
|
text: "文件读取中",
|
||||||
|
spinner: "el-icon-loading",
|
||||||
|
background: "rgba(0, 0, 0, 0.7)"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (window.vant) {
|
||||||
|
return vant.Toast.loading({
|
||||||
|
message: "文件读取中",
|
||||||
|
forbidClick: true,
|
||||||
|
overlay: true,
|
||||||
|
duration: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeImportLoading = (loading) => {
|
||||||
|
if (loading && loading.close) {
|
||||||
|
loading.close()
|
||||||
|
} else if (window.vant && vant.Toast.clear) {
|
||||||
|
vant.Toast.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showImportError = (message) => {
|
||||||
|
if (window.ELEMENT && ELEMENT.Message) {
|
||||||
|
ELEMENT.Message.error(message)
|
||||||
|
} else if (window.vant) {
|
||||||
|
vant.Toast(message)
|
||||||
|
} else {
|
||||||
|
alert(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chooseImportFile = (accept, fileHandler) => {
|
||||||
|
const input = document.createElement("input")
|
||||||
|
input.type = "file"
|
||||||
|
input.accept = accept
|
||||||
|
input.style.display = "none"
|
||||||
|
input.addEventListener("change", (event) => {
|
||||||
|
const files = event.target.files
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
input.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileHandler(files[0]).finally(() => {
|
||||||
|
input.remove()
|
||||||
|
})
|
||||||
|
}, {once: true})
|
||||||
|
document.body.appendChild(input)
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const importEditorDocument = (editor, file, url, errorMessage) => {
|
||||||
|
const loading = showImportLoading()
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("file", file)
|
||||||
|
return axios.post(url, formData, {
|
||||||
|
headers: {"Content-Type": "multipart/form-data", "x-requested-with": "XMLHttpRequest"}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
editor.txt.html(response.data.data || "")
|
||||||
|
} else {
|
||||||
|
showImportError(response.data.msg || errorMessage)
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
showImportError(errorMessage)
|
||||||
|
}).finally(() => {
|
||||||
|
closeImportLoading(loading)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
class PDFMenu extends BtnMenu {
|
class PDFMenu extends BtnMenu {
|
||||||
constructor(editor) {
|
constructor(editor) {
|
||||||
const $elem = wangEditor.$(
|
const $elem = wangEditor.$(
|
||||||
`<div class="w-e-menu" data-title="上传pdf">
|
'<div class="w-e-menu" data-title="上传PDF"><i class="fa fa-file-pdf-o" aria-hidden="true"></i></div>'
|
||||||
<i class="fa fa-file-pdf-o" aria-hidden="true" /></i>
|
|
||||||
</div>`
|
|
||||||
)
|
)
|
||||||
super($elem, editor)
|
super($elem, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
clickHandler() {
|
clickHandler() {
|
||||||
const _this = this
|
chooseImportFile("application/pdf,.pdf", (file) => {
|
||||||
const inputFileElement = document.querySelector('#'+this.editor.textElemId).parentElement.parentElement.nextElementSibling
|
if (!file.name.toLowerCase().endsWith(".pdf")) {
|
||||||
inputFileElement.click()
|
showImportError("请上传PDF文件")
|
||||||
inputFileElement.addEventListener('change', function () {
|
return Promise.resolve()
|
||||||
const file = this.files[0];
|
}
|
||||||
const fileName = file.name
|
if (this.editor.config.officeImageImport) {
|
||||||
|
return importEditorDocument(this.editor, file,
|
||||||
if (!fileName.toLowerCase().endsWith('.pdf')) {
|
"/platform/sys/file/convertPdfToImageHtml", "PDF导入失败,请联系管理员")
|
||||||
alert('请上传pdf格式的文件')
|
|
||||||
_this.clearInputFile()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loading = showImportLoading()
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file, file.name)
|
formData.append("file", file, file.name)
|
||||||
/*axios.post("/platform/sys/file/uploadDynamicReturnUrl", formData, {
|
return axios.post("/platform/sys/file/uploadDynamicReturnUrl", formData, {})
|
||||||
headers: {
|
.then((response) => {
|
||||||
"Content-Type": "multipart/form-data"
|
|
||||||
}
|
|
||||||
})*/
|
|
||||||
axios.post("/platform/sys/file/uploadDynamicReturnUrl", formData, {}).then(response => {
|
|
||||||
console.log(response.data)
|
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
const html = `<a target="_blank" href="${window.location.origin}${response.data.data}">
|
const html = '<a target="_blank" href="' + window.location.origin
|
||||||
${file.name}
|
+ response.data.data + '">' + file.name + '</a>'
|
||||||
</a>`
|
this.editor.txt.html(html)
|
||||||
_this.editor.txt.html(html)
|
|
||||||
} else {
|
} else {
|
||||||
alert(response.data.msg + ',上传pdf失败,请联系管理员!')
|
showImportError(response.data.msg + ",上传PDF失败,请联系管理员")
|
||||||
_this.clearInputFile()
|
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(() => {
|
||||||
console.log(error)
|
showImportError("上传PDF失败,请联系管理员")
|
||||||
_this.clearInputFile()
|
}).finally(() => {
|
||||||
alert('上传pdf失败,请联系管理员!')
|
closeImportLoading(loading)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}
|
|
||||||
|
|
||||||
clearInputFile() {
|
|
||||||
const obj = document.getElementById('fileInput');
|
|
||||||
if(obj) {
|
|
||||||
obj.outerHTML = obj.outerHTML
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tryChangeActive() {
|
|
||||||
this.active()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tryChangeActive() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
//注册上传word按钮
|
//注册上传word按钮
|
||||||
@@ -83,70 +137,38 @@ class DocMenu extends BtnMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clickHandler() {
|
clickHandler() {
|
||||||
const fileInputId = this.editor.textElemId + "input-file"
|
chooseImportFile("application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,.doc,.docx", (file) => {
|
||||||
const existingInput = document.getElementById(fileInputId)
|
return importEditorDocument(this.editor, file,
|
||||||
if (existingInput) {
|
"/platform/sys/file/convertHtml", "Word导入失败,请联系管理员")
|
||||||
existingInput.remove()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = document.createElement("input")
|
tryChangeActive() {}
|
||||||
input.type = "file"
|
}
|
||||||
input.accept = "application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
||||||
input.id = fileInputId
|
class ExcelMenu extends BtnMenu {
|
||||||
input.style.display = "none"
|
constructor(editor) {
|
||||||
input.addEventListener("change", (event) => {
|
const displayStyle = editor.config.officeImageImport ? "" : "display:none;"
|
||||||
const files = event.target.files
|
const $elem = wangEditor.$(
|
||||||
if (!files || files.length === 0) {
|
'<div class="w-e-menu" data-title="上传Excel" style="' + displayStyle
|
||||||
|
+ '"><i class="fa fa-file-excel-o" aria-hidden="true"></i></div>'
|
||||||
|
)
|
||||||
|
super($elem, editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
clickHandler() {
|
||||||
|
if (!this.editor.config.officeImageImport) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
chooseImportFile("application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,.xls,.xlsx", (file) => {
|
||||||
let loading = null
|
const fileName = file.name.toLowerCase()
|
||||||
if (window.ELEMENT) {
|
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
|
||||||
loading = ELEMENT.Loading.service({
|
showImportError("请上传xls或xlsx文件")
|
||||||
lock: true,
|
return Promise.resolve()
|
||||||
text: "文件读取中",
|
|
||||||
spinner: "el-icon-loading",
|
|
||||||
background: "rgba(0, 0, 0, 0.7)"
|
|
||||||
})
|
|
||||||
} else if (window.vant) {
|
|
||||||
loading = vant.Toast.loading({
|
|
||||||
message: "文件读取中",
|
|
||||||
forbidClick: true,
|
|
||||||
overlay: true,
|
|
||||||
duration: 0
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return importEditorDocument(this.editor, file,
|
||||||
const formData = new FormData()
|
"/platform/sys/file/convertExcelToHtml", "Excel导入失败,请联系管理员")
|
||||||
formData.append("file", files[0])
|
|
||||||
axios
|
|
||||||
.post("/platform/sys/file/convertHtml", formData, {
|
|
||||||
headers: { "Content-Type": "multipart/form-data", "x-requested-with": "XMLHttpRequest" }
|
|
||||||
})
|
})
|
||||||
.then((response) => {
|
|
||||||
loading.close()
|
|
||||||
if (response.data.code === 0) {
|
|
||||||
this.editor.txt.html(response.data.data)
|
|
||||||
} else {
|
|
||||||
if (window.ELEMENT) {
|
|
||||||
this.$message.error(response.data.msg)
|
|
||||||
} else if (window.vant) {
|
|
||||||
vant.Toast(response.data.msg)
|
|
||||||
} else {
|
|
||||||
alert(response.data.msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log(error)
|
|
||||||
alert("上传word失败,请联系管理员!")
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
input.remove()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
input.click()
|
|
||||||
document.body.appendChild(input)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tryChangeActive() {}
|
tryChangeActive() {}
|
||||||
@@ -154,6 +176,7 @@ class DocMenu extends BtnMenu {
|
|||||||
|
|
||||||
wangEditor.registerMenu("docMenu", DocMenu)
|
wangEditor.registerMenu("docMenu", DocMenu)
|
||||||
wangEditor.registerMenu('pdfMenuKey', PDFMenu)
|
wangEditor.registerMenu('pdfMenuKey', PDFMenu)
|
||||||
|
wangEditor.registerMenu("excelMenu", ExcelMenu)
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "textEditor",
|
name: "textEditor",
|
||||||
props: {
|
props: {
|
||||||
@@ -170,6 +193,10 @@ module.exports = {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 300
|
default: 300
|
||||||
},
|
},
|
||||||
|
officeImageImport: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
menus: {
|
menus: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => {
|
default: () => {
|
||||||
@@ -214,7 +241,7 @@ module.exports = {
|
|||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
value: {
|
value: {
|
||||||
handler: function (newValue) {
|
handler(newValue) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const content = newValue || ""
|
const content = newValue || ""
|
||||||
if (this.editor.txt.html() !== content) {
|
if (this.editor.txt.html() !== content) {
|
||||||
@@ -237,6 +264,8 @@ module.exports = {
|
|||||||
let _this = this
|
let _this = this
|
||||||
this.editor = new wangEditor(this.$refs.editorRef)
|
this.editor = new wangEditor(this.$refs.editorRef)
|
||||||
this.editor.config.focus = false
|
this.editor.config.focus = false
|
||||||
|
// 仅指定业务启用PDF转图片和Excel转表格,其他富文本页面保持原有导入行为。
|
||||||
|
this.editor.config.officeImageImport = this.officeImageImport
|
||||||
//内容change回调
|
//内容change回调
|
||||||
this.editor.config.onchange = (newHtml) => {
|
this.editor.config.onchange = (newHtml) => {
|
||||||
this.cursorPos = this.editor.selection.getCursorPos()
|
this.cursorPos = this.editor.selection.getCursorPos()
|
||||||
@@ -244,13 +273,13 @@ module.exports = {
|
|||||||
range.setStart(range.startContainer, Math.max(this.cursorPos, range.endOffset))
|
range.setStart(range.startContainer, Math.max(this.cursorPos, range.endOffset))
|
||||||
this.$emit("input", newHtml)
|
this.$emit("input", newHtml)
|
||||||
}
|
}
|
||||||
this.editor.config.linkImgCheck = function (imgSrc, alt, href) {
|
this.editor.config.linkImgCheck = (imgSrc, alt, href) => {
|
||||||
// 1. 返回 true ,说明检查通过
|
// 1. 返回 true ,说明检查通过
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
//配置图片上传
|
//配置图片上传
|
||||||
this.editor.config.customUploadImg = function (resultFiles, insertImgFn) {
|
this.editor.config.customUploadImg = (resultFiles, insertImgFn) => {
|
||||||
// resultFiles 是 input 中选中的文件列表
|
// resultFiles 是 input 中选中的文件列表
|
||||||
// insertImgFn 是获取图片 url 后,插入到编辑器的方法
|
// insertImgFn 是获取图片 url 后,插入到编辑器的方法
|
||||||
Promise.all(_this.uploadFiles(resultFiles)).then((res) => {
|
Promise.all(_this.uploadFiles(resultFiles)).then((res) => {
|
||||||
@@ -269,7 +298,7 @@ module.exports = {
|
|||||||
this.editor.create()
|
this.editor.create()
|
||||||
},
|
},
|
||||||
uploadFiles(resultFiles) {
|
uploadFiles(resultFiles) {
|
||||||
return resultFiles.map(async (file) => {
|
return resultFiles.map((file) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append("file", file)
|
formData.append("file", file)
|
||||||
return axios.post("/platform/sys/file/uploadDynamicReturnUrl", formData, {
|
return axios.post("/platform/sys/file/uploadDynamicReturnUrl", formData, {
|
||||||
|
|||||||
@@ -347,6 +347,19 @@ module.exports = {
|
|||||||
|
|
||||||
// 预览图片
|
// 预览图片
|
||||||
handlePreview(file) {
|
handlePreview(file) {
|
||||||
|
if (this.upload_mode === "image") {
|
||||||
|
// 回显记录的名称可能是无扩展名的下载地址;图片模式直接预览缩略图地址。
|
||||||
|
const url = file.url || (this.upload_result_type === "url" && file.response?.data)
|
||||||
|
if (!url) {
|
||||||
|
this.$message.warning("暂无可预览的图片")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const image = new Image()
|
||||||
|
image.src = url
|
||||||
|
const viewer = new Viewer(image, {zIndex: 99999, title: false, hidden: () => viewer.destroy()})
|
||||||
|
viewer.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
const suffix = file.name.substring(file.name.lastIndexOf(".") + 1).toLowerCase()
|
const suffix = file.name.substring(file.name.lastIndexOf(".") + 1).toLowerCase()
|
||||||
const fileId = file.response.data.substring(file.response.data.lastIndexOf("=") + 1)
|
const fileId = file.response.data.substring(file.response.data.lastIndexOf("=") + 1)
|
||||||
const buildFile = {
|
const buildFile = {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user