Compare commits
2 Commits
v0.1.0
...
ac7eb6d85d
| Author | SHA1 | Date | |
|---|---|---|---|
| ac7eb6d85d | |||
| 728847a8e3 |
@@ -5,6 +5,7 @@ import com.easycard.common.auth.JwtTokenService;
|
||||
import com.easycard.common.auth.LoginUser;
|
||||
import com.easycard.common.tenant.TenantContext;
|
||||
import com.easycard.common.tenant.TenantContextHolder;
|
||||
import com.easycard.module.tenant.web.MiniappTenantContextFilter;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
@@ -83,7 +84,8 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
HttpSecurity http,
|
||||
JwtAuthenticationFilter jwtAuthenticationFilter
|
||||
JwtAuthenticationFilter jwtAuthenticationFilter,
|
||||
MiniappTenantContextFilter miniappTenantContextFilter
|
||||
) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
@@ -108,6 +110,7 @@ public class SecurityConfig {
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.getWriter().write("{\"code\":\"UNAUTHORIZED\",\"message\":\"未登录或登录已失效\",\"data\":null}");
|
||||
}))
|
||||
.addFilterBefore(miniappTenantContextFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.cors(Customizer.withDefaults());
|
||||
return http.build();
|
||||
@@ -128,7 +131,10 @@ class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI();
|
||||
return uri.startsWith("/api/open/") || "/api/v1/auth/login".equals(uri);
|
||||
if (uri == null) {
|
||||
return false;
|
||||
}
|
||||
return uri.contains("/api/open/") || uri.endsWith("/api/v1/auth/login");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -32,9 +32,4 @@ public class GlobalExceptionHandler {
|
||||
public ApiResponse<Void> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException exception) {
|
||||
return ApiResponse.fail("FILE_TOO_LARGE", "上传图片不能超过 5MB");
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ApiResponse<Void> handleException(Exception exception) {
|
||||
return ApiResponse.fail("INTERNAL_SERVER_ERROR", exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,18 +343,21 @@ public class CardProfileService {
|
||||
Map<Long, List<String>> specialtyMap = loadSpecialtyMap(cards.stream().map(CardProfileDO::getId).toList());
|
||||
return cards.stream()
|
||||
.filter(card -> {
|
||||
String deptName = deptMap.containsKey(card.getDeptId()) ? deptMap.get(card.getDeptId()).getDeptName() : "";
|
||||
OrgDepartmentDO department = card.getDeptId() == null ? null : deptMap.get(card.getDeptId());
|
||||
String deptName = department == null ? "" : department.getDeptName();
|
||||
List<String> specialties = specialtyMap.getOrDefault(card.getId(), List.of());
|
||||
boolean keywordMatched = !StringUtils.hasText(keyword)
|
||||
|| card.getCardName().contains(keyword)
|
||||
|| deptName.contains(keyword)
|
||||
|| specialties.stream().anyMatch(item -> item.contains(keyword));
|
||||
|| containsText(card.getCardName(), keyword)
|
||||
|| containsText(deptName, keyword)
|
||||
|| specialties.stream().anyMatch(item -> containsText(item, keyword));
|
||||
boolean officeMatched = !StringUtils.hasText(office) || office.equals(deptName);
|
||||
boolean areaMatched = !StringUtils.hasText(practiceArea) || specialties.stream().anyMatch(item -> item.equals(practiceArea));
|
||||
boolean areaMatched = !StringUtils.hasText(practiceArea)
|
||||
|| specialties.stream().anyMatch(item -> equalsText(item, practiceArea));
|
||||
return keywordMatched && officeMatched && areaMatched;
|
||||
})
|
||||
.map(card -> {
|
||||
String deptName = deptMap.containsKey(card.getDeptId()) ? deptMap.get(card.getDeptId()).getDeptName() : "";
|
||||
OrgDepartmentDO department = card.getDeptId() == null ? null : deptMap.get(card.getDeptId());
|
||||
String deptName = department == null ? "" : department.getDeptName();
|
||||
return new OpenCardListItem(
|
||||
card.getId(),
|
||||
card.getCardName(),
|
||||
@@ -496,6 +499,14 @@ public class CardProfileService {
|
||||
return AUTO_MANAGED_ROLE_CODE.equals(roleCode);
|
||||
}
|
||||
|
||||
private boolean containsText(String source, String keyword) {
|
||||
return source != null && keyword != null && source.contains(keyword);
|
||||
}
|
||||
|
||||
private boolean equalsText(String left, String right) {
|
||||
return left != null && left.equals(right);
|
||||
}
|
||||
|
||||
private SysUserDO createHiddenLawyerUser(LoginUser loginUser, UpsertCardRequest request) {
|
||||
SysRoleDO role = getRequiredTenantRole(loginUser.tenantId(), AUTO_MANAGED_ROLE_CODE);
|
||||
SysUserDO user = new SysUserDO();
|
||||
|
||||
@@ -17,6 +17,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
@@ -58,6 +59,7 @@ public class TenantOrgService {
|
||||
profile.setCreatedBy(loginUser.userId());
|
||||
profile.setDeleted(0);
|
||||
}
|
||||
CoordinateValue coordinates = parseCoordinates(request.hqAddress(), request.hqLatitude(), request.hqLongitude());
|
||||
profile.setFirmName(request.firmName());
|
||||
profile.setFirmShortName(request.firmShortName());
|
||||
profile.setEnglishName(request.englishName());
|
||||
@@ -68,8 +70,8 @@ public class TenantOrgService {
|
||||
profile.setWebsiteUrl(request.websiteUrl());
|
||||
profile.setWechatOfficialAccount(request.wechatOfficialAccount());
|
||||
profile.setHqAddress(request.hqAddress());
|
||||
profile.setHqLatitude(toBigDecimal(request.hqLatitude()));
|
||||
profile.setHqLongitude(toBigDecimal(request.hqLongitude()));
|
||||
profile.setHqLatitude(coordinates.latitude());
|
||||
profile.setHqLongitude(coordinates.longitude());
|
||||
profile.setUpdatedBy(loginUser.userId());
|
||||
if (profile.getId() == null) {
|
||||
orgFirmProfileMapper.insert(profile);
|
||||
@@ -217,11 +219,51 @@ public class TenantOrgService {
|
||||
);
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
private CoordinateValue parseCoordinates(String address, String latitude, String longitude) {
|
||||
String latitudeText = normalizeCoordinate(latitude);
|
||||
String longitudeText = normalizeCoordinate(longitude);
|
||||
boolean hasLatitude = StringUtils.hasText(latitudeText);
|
||||
boolean hasLongitude = StringUtils.hasText(longitudeText);
|
||||
|
||||
if (!hasLatitude && !hasLongitude) {
|
||||
return new CoordinateValue(null, null);
|
||||
}
|
||||
if (!hasLatitude || !hasLongitude) {
|
||||
throw new BusinessException("FIRM_COORDINATE_INVALID", "纬度和经度需同时填写");
|
||||
}
|
||||
if (!StringUtils.hasText(address)) {
|
||||
throw new BusinessException("FIRM_ADDRESS_REQUIRED", "填写地图坐标时请同时填写详细地址");
|
||||
}
|
||||
|
||||
BigDecimal latitudeValue = parseCoordinateValue(latitudeText, "纬度");
|
||||
BigDecimal longitudeValue = parseCoordinateValue(longitudeText, "经度");
|
||||
|
||||
if (!isInRange(latitudeValue, -90, 90)) {
|
||||
if (isInRange(latitudeValue, -180, 180) && isInRange(longitudeValue, -90, 90)) {
|
||||
throw new BusinessException("FIRM_COORDINATE_INVALID", "纬度超出范围,请检查是否与经度填反");
|
||||
}
|
||||
throw new BusinessException("FIRM_COORDINATE_INVALID", "纬度范围应在 -90 到 90 之间");
|
||||
}
|
||||
if (!isInRange(longitudeValue, -180, 180)) {
|
||||
throw new BusinessException("FIRM_COORDINATE_INVALID", "经度范围应在 -180 到 180 之间");
|
||||
}
|
||||
return new CoordinateValue(latitudeValue, longitudeValue);
|
||||
}
|
||||
|
||||
private String normalizeCoordinate(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private BigDecimal parseCoordinateValue(String value, String fieldLabel) {
|
||||
try {
|
||||
return new BigDecimal(value);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BusinessException("FIRM_COORDINATE_INVALID", fieldLabel + "必须是数字");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInRange(BigDecimal value, int min, int max) {
|
||||
return value.compareTo(BigDecimal.valueOf(min)) >= 0 && value.compareTo(BigDecimal.valueOf(max)) <= 0;
|
||||
}
|
||||
|
||||
private String resolveAssetUrl(Long assetId) {
|
||||
@@ -270,6 +312,9 @@ public class TenantOrgService {
|
||||
) {
|
||||
}
|
||||
|
||||
private record CoordinateValue(BigDecimal latitude, BigDecimal longitude) {
|
||||
}
|
||||
|
||||
public record PracticeAreaView(Long id, String areaCode, String areaName, Integer displayOrder, String areaStatus) {
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ public class MiniappTenantContextFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
return !request.getRequestURI().startsWith("/api/open/");
|
||||
String uri = request.getRequestURI();
|
||||
return uri == null || !uri.contains("/api/open/");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { UploadRequestOptions } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
@@ -19,6 +20,7 @@ function generateDeptCode() {
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const imageUploading = reactive({
|
||||
logo: false,
|
||||
hero: false,
|
||||
@@ -72,6 +74,95 @@ const deptOptions = computed(() => departments.value.map(item => ({
|
||||
value: item.id,
|
||||
})))
|
||||
|
||||
function normalizeText(value: string | null | undefined) {
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
}
|
||||
|
||||
function parseCoordinate(value: string | null | undefined) {
|
||||
const normalized = normalizeText(value)
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
const parsed = Number(normalized)
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
function hasCoordinateInput() {
|
||||
return Boolean(normalizeText(profile.hqLatitude) || normalizeText(profile.hqLongitude))
|
||||
}
|
||||
|
||||
function getCoordinateValidationMessage() {
|
||||
const latitudeText = normalizeText(profile.hqLatitude)
|
||||
const longitudeText = normalizeText(profile.hqLongitude)
|
||||
const hasLatitude = Boolean(latitudeText)
|
||||
const hasLongitude = Boolean(longitudeText)
|
||||
|
||||
if (!hasLatitude && !hasLongitude) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!hasLatitude || !hasLongitude) {
|
||||
return '纬度和经度需同时填写'
|
||||
}
|
||||
|
||||
const latitude = parseCoordinate(profile.hqLatitude)
|
||||
if (latitude === null) {
|
||||
return '纬度必须是数字'
|
||||
}
|
||||
|
||||
const longitude = parseCoordinate(profile.hqLongitude)
|
||||
if (longitude === null) {
|
||||
return '经度必须是数字'
|
||||
}
|
||||
|
||||
if (latitude < -90 || latitude > 90) {
|
||||
if (latitude >= -180 && latitude <= 180 && longitude >= -90 && longitude <= 90) {
|
||||
return '纬度超出范围,请检查是否与经度填反'
|
||||
}
|
||||
return '纬度范围应在 -90 到 90 之间'
|
||||
}
|
||||
|
||||
if (longitude < -180 || longitude > 180) {
|
||||
return '经度范围应在 -180 到 180 之间'
|
||||
}
|
||||
|
||||
if (!normalizeText(profile.hqAddress)) {
|
||||
return '填写地图坐标时请同时填写详细地址'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const profileRules: FormRules = {
|
||||
firmName: [{ required: true, message: '请输入机构名称', trigger: 'blur' }],
|
||||
hqAddress: [{
|
||||
trigger: ['blur', 'change'],
|
||||
validator: async () => {
|
||||
if (hasCoordinateInput() && !normalizeText(profile.hqAddress)) {
|
||||
throw new Error('填写地图坐标时请同时填写详细地址')
|
||||
}
|
||||
},
|
||||
}],
|
||||
hqLatitude: [{
|
||||
trigger: ['blur', 'change'],
|
||||
validator: async () => {
|
||||
const message = getCoordinateValidationMessage()
|
||||
if (message) {
|
||||
throw new Error(message)
|
||||
}
|
||||
},
|
||||
}],
|
||||
hqLongitude: [{
|
||||
trigger: ['blur', 'change'],
|
||||
validator: async () => {
|
||||
const message = getCoordinateValidationMessage()
|
||||
if (message) {
|
||||
throw new Error(message)
|
||||
}
|
||||
},
|
||||
}],
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -135,6 +226,11 @@ function handleHeroUpload(options: UploadRequestOptions) {
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
const valid = await formRef.value?.validate().then(() => true).catch(() => false)
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const saved = await tenantApi.saveFirmProfile(profile)
|
||||
@@ -267,9 +363,15 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top" class="card-form material-form">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="profile"
|
||||
:rules="profileRules"
|
||||
label-position="top"
|
||||
class="card-form material-form"
|
||||
>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="名称">
|
||||
<el-form-item label="名称" prop="firmName">
|
||||
<el-input v-model="profile.firmName" class="material-input" placeholder="完整的机构注册名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="简称">
|
||||
@@ -284,13 +386,13 @@ onMounted(() => {
|
||||
<el-form-item label="官网地址">
|
||||
<el-input v-model="profile.websiteUrl" class="material-input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="详细地址" class="form-grid__full">
|
||||
<el-form-item label="详细地址" prop="hqAddress" class="form-grid__full">
|
||||
<el-input v-model="profile.hqAddress" class="material-input" placeholder="完整的真实办公地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="纬度 (LAT)">
|
||||
<el-form-item label="纬度 (LAT)" prop="hqLatitude">
|
||||
<el-input v-model="profile.hqLatitude" class="material-input" placeholder="例如:31.230416" />
|
||||
</el-form-item>
|
||||
<el-form-item label="经度 (LNG)">
|
||||
<el-form-item label="经度 (LNG)" prop="hqLongitude">
|
||||
<el-input v-model="profile.hqLongitude" class="material-input" placeholder="例如:121.473701" />
|
||||
</el-form-item>
|
||||
<el-form-item label="机构简介" class="form-grid__full">
|
||||
|
||||
@@ -8,8 +8,8 @@ interface OpenFirmResponse {
|
||||
intro: string;
|
||||
hotlinePhone: string;
|
||||
hqAddress: string;
|
||||
hqLatitude: number;
|
||||
hqLongitude: number;
|
||||
hqLatitude: number | string | null;
|
||||
hqLongitude: number | string | null;
|
||||
officeCount: number;
|
||||
lawyerCount: number;
|
||||
officeList: string[];
|
||||
@@ -42,11 +42,62 @@ interface OpenCardDetailResponse {
|
||||
specialties: string[];
|
||||
firmName: string;
|
||||
firmAddress: string;
|
||||
firmLatitude: number;
|
||||
firmLongitude: number;
|
||||
firmLatitude: number | string | null;
|
||||
firmLongitude: number | string | null;
|
||||
}
|
||||
|
||||
function parseCoordinate(value: number | string | null | undefined): number | null {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function isLatitude(value: number | null): value is number {
|
||||
return value !== null && value >= -90 && value <= 90;
|
||||
}
|
||||
|
||||
function isLongitude(value: number | null): value is number {
|
||||
return value !== null && value >= -180 && value <= 180;
|
||||
}
|
||||
|
||||
function normalizeCoordinates(latitude: number | string | null | undefined, longitude: number | string | null | undefined): {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
} {
|
||||
const parsedLatitude = parseCoordinate(latitude);
|
||||
const parsedLongitude = parseCoordinate(longitude);
|
||||
|
||||
if (isLatitude(parsedLatitude) && isLongitude(parsedLongitude)) {
|
||||
return {
|
||||
latitude: parsedLatitude,
|
||||
longitude: parsedLongitude,
|
||||
};
|
||||
}
|
||||
|
||||
if (isLatitude(parsedLongitude) && isLongitude(parsedLatitude)) {
|
||||
return {
|
||||
latitude: parsedLongitude,
|
||||
longitude: parsedLatitude,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function toFirmInfo(payload: OpenFirmResponse): FirmInfo {
|
||||
const coordinates = normalizeCoordinates(payload.hqLatitude, payload.hqLongitude);
|
||||
return {
|
||||
id: payload.name || 'firm',
|
||||
name: payload.name || '',
|
||||
@@ -54,8 +105,8 @@ function toFirmInfo(payload: OpenFirmResponse): FirmInfo {
|
||||
intro: payload.intro || '',
|
||||
hotlinePhone: payload.hotlinePhone || '',
|
||||
hqAddress: payload.hqAddress || '',
|
||||
hqLatitude: payload.hqLatitude || 0,
|
||||
hqLongitude: payload.hqLongitude || 0,
|
||||
hqLatitude: coordinates.latitude,
|
||||
hqLongitude: coordinates.longitude,
|
||||
officeCount: payload.officeCount || 0,
|
||||
lawyerCount: payload.lawyerCount || 0,
|
||||
heroImage: payload.heroImage || '',
|
||||
@@ -114,6 +165,7 @@ export async function getLawyerDetail(cardId: string, sourceType = 'DIRECT', sha
|
||||
const payload = await request<OpenCardDetailResponse>({
|
||||
url: `/api/open/cards/${encodeURIComponent(cardId)}?${queryParts.join('&')}`,
|
||||
});
|
||||
const coordinates = normalizeCoordinates(payload.firmLatitude, payload.firmLongitude);
|
||||
|
||||
return {
|
||||
firm: {
|
||||
@@ -123,8 +175,8 @@ export async function getLawyerDetail(cardId: string, sourceType = 'DIRECT', sha
|
||||
intro: '',
|
||||
hotlinePhone: '',
|
||||
hqAddress: payload.firmAddress || '',
|
||||
hqLatitude: payload.firmLatitude || 0,
|
||||
hqLongitude: payload.firmLongitude || 0,
|
||||
hqLatitude: coordinates.latitude,
|
||||
hqLongitude: coordinates.longitude,
|
||||
officeCount: 0,
|
||||
lawyerCount: 0,
|
||||
heroImage: '',
|
||||
|
||||
@@ -9,8 +9,8 @@ export interface TenantRuntimeConfig {
|
||||
// develop 环境允许本地联调;trial/release 请替换为已备案且已配置到小程序后台“服务器域名”的 HTTPS 域名。
|
||||
export const tenantRuntimeConfig: TenantRuntimeConfig = {
|
||||
apiBaseUrlByEnv: {
|
||||
develop: 'http://127.0.0.1:8112',
|
||||
trial: 'https://trial-api.example.com',
|
||||
release: 'https://api.example.com',
|
||||
develop: 'https://easyflowtech.cn/card',
|
||||
trial: 'https://easyflowtech.cn/card',
|
||||
release: 'https://easyflowtech.cn/card',
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user