修改定位信息 0323

main
kangwenjing 2026-03-23 15:21:09 +08:00
parent 512f42dfd4
commit 13d3abeeee
20 changed files with 196 additions and 1709 deletions

View File

@ -43,7 +43,6 @@ import org.springframework.web.multipart.MultipartFile;
@Service @Service
public class WorkServiceImpl implements WorkService { public class WorkServiceImpl implements WorkService {
private static final String NOMINATIM_BASE_URL = "https://nominatim.openstreetmap.org/reverse";
private static final String PHOTO_METADATA_PREFIX = "[[CHECKIN_PHOTOS]]"; private static final String PHOTO_METADATA_PREFIX = "[[CHECKIN_PHOTOS]]";
private static final String PHOTO_METADATA_SUFFIX = "[[/CHECKIN_PHOTOS]]"; private static final String PHOTO_METADATA_SUFFIX = "[[/CHECKIN_PHOTOS]]";
private static final Pattern PHOTO_METADATA_PATTERN = Pattern.compile("\\[\\[CHECKIN_PHOTOS]](.*?)\\[\\[/CHECKIN_PHOTOS]]", Pattern.DOTALL); private static final Pattern PHOTO_METADATA_PATTERN = Pattern.compile("\\[\\[CHECKIN_PHOTOS]](.*?)\\[\\[/CHECKIN_PHOTOS]]", Pattern.DOTALL);
@ -129,49 +128,7 @@ public class WorkServiceImpl implements WorkService {
if (latitude == null || longitude == null) { if (latitude == null || longitude == null) {
throw new BusinessException("定位坐标不能为空"); throw new BusinessException("定位坐标不能为空");
} }
throw new BusinessException("当前环境未启用服务端逆地理解析");
try {
String requestUrl = NOMINATIM_BASE_URL
+ "?format=jsonv2&addressdetails=1&namedetails=1&extratags=1&zoom=19"
+ "&lat=" + URLEncoder.encode(latitude.stripTrailingZeros().toPlainString(), StandardCharsets.UTF_8)
+ "&lon=" + URLEncoder.encode(longitude.stripTrailingZeros().toPlainString(), StandardCharsets.UTF_8)
+ "&accept-language=" + URLEncoder.encode("zh-CN,zh;q=0.9,en;q=0.8", StandardCharsets.UTF_8);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(requestUrl))
.header("Accept", "application/json")
.header("User-Agent", "unis-crm-backend/1.0 (workbench reverse geocoding)")
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new BusinessException("地点解析失败,请稍后重试");
}
JsonNode root = objectMapper.readTree(response.body());
JsonNode addressNode = root.path("address");
String orderedLocation = buildOrderedLocationName(root, addressNode);
if (orderedLocation != null) {
return orderedLocation;
}
String displayName = textValue(root, "display_name");
if (displayName != null) {
String normalizedDisplayName = normalizeDisplayName(displayName);
if (normalizedDisplayName != null) {
return normalizedDisplayName;
}
return displayName;
}
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException("地点解析失败,请检查网络后重试");
}
throw new BusinessException("未能解析出具体地点名称");
} }
@Override @Override

View File

@ -1,60 +0,0 @@
server:
port: 8080
spring:
application:
name: unis-crm-backend
servlet:
multipart:
max-file-size: 20MB
max-request-size: 25MB
datasource:
url: jdbc:postgresql://127.0.0.1:5432/nex_auth
username: postgres
password: 199628
driver-class-name: org.postgresql.Driver
data:
redis:
host: 127.0.0.1
port: 6379
password: 199628@tlw
database: 14
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.unis.crm.dto.dashboard
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
com.unis.crm: info
unisbase:
tenant:
enabled: false
web:
auth-endpoints-enabled: true
management-endpoints-enabled: true
security:
enabled: true
mode: embedded
jwt-secret: change-me-please-change-me-32bytes
auth-header: Authorization
token-prefix: "Bearer "
permit-all-urls:
- /actuator/health
internal-auth:
enabled: true
secret: change-me-internal-secret
header-name: X-Internal-Secret
app:
upload-path: /Users/kangwenjing/Downloads/crm/uploads
resource-prefix: /sys/api/static/
captcha:
ttl-seconds: 120
max-attempts: 5
token:
access-default-minutes: 30
refresh-default-days: 7

View File

@ -1,206 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unis.crm.mapper.DashboardMapper">
<select id="selectDefaultUserId" resultType="java.lang.Long">
select user_id
from sys_user
where status = 1
order by user_id asc
limit 1
</select>
<select id="selectUserWelcome" resultType="com.unis.crm.dto.dashboard.UserWelcomeDTO">
select
u.user_id as userId,
u.display_name as realName,
null as jobTitle,
null as deptName,
null as hireDate
from sys_user u
where u.user_id = #{userId}
and u.status = 1
limit 1
</select>
<select id="selectDashboardStats" resultType="com.unis.crm.dto.dashboard.DashboardStatDTO">
select '本月新增商机' as name,
count(1)::bigint as value,
'monthlyOpportunities' as metricKey
from crm_opportunity
where owner_user_id = #{userId}
and date_trunc('month', created_at) = date_trunc('month', now())
union all
select '跟进中客户' as name,
count(1)::bigint as value,
'followingCustomers' as metricKey
from crm_customer
where owner_user_id = #{userId}
and status = 'following'
union all
select '已成单项目' as name,
count(1)::bigint as value,
'wonProjects' as metricKey
from crm_opportunity
where owner_user_id = #{userId}
and stage = 'won'
union all
select '本月打卡天数' as name,
count(distinct checkin_date)::bigint as value,
'monthlyCheckins' as metricKey
from work_checkin
where user_id = #{userId}
and date_trunc('month', checkin_date::timestamp) = date_trunc('month', now())
</select>
<select id="selectPendingTodos" resultType="com.unis.crm.dto.dashboard.DashboardTodoDTO">
select
id,
title,
biz_type as bizType,
biz_id as bizId,
priority,
status,
due_date as dueDate,
created_at as createdAt
from work_todo
where user_id = #{userId}
and status = 'todo'
order by
case priority
when 'high' then 1
when 'medium' then 2
else 3
end,
coalesce(due_date, created_at) asc
limit 6
</select>
<select id="selectLatestActivities" resultType="com.unis.crm.dto.dashboard.DashboardActivityDTO">
with latest_report_comment as (
select distinct on (c.report_id)
c.report_id,
c.reviewer_user_id,
c.score,
c.comment_content,
c.reviewed_at
from work_daily_report_comment c
order by c.report_id, c.reviewed_at desc, c.id desc
),
activity_union as (
select
l.id,
l.biz_type as bizType,
l.biz_id as bizId,
l.action_type as actionType,
l.title,
l.content,
l.operator_user_id as operatorUserId,
l.created_at as createdAt
from sys_activity_log l
where l.operator_user_id = #{userId}
or l.operator_user_id is null
union all
select
(1000000000 + o.id) as id,
'opportunity' as bizType,
o.id as bizId,
'stage_update' as actionType,
'商机阶段更新' as title,
o.opportunity_name || ' 已推进至' ||
case o.stage
when 'initial_contact' then '初步沟通'
when 'solution_discussion' then '方案交流'
when 'bidding' then '招投标'
when 'business_negotiation' then '商务谈判'
when 'won' then '已成交'
when 'lost' then '已输单'
else o.stage
end || '阶段' as content,
o.owner_user_id as operatorUserId,
o.updated_at as createdAt
from crm_opportunity o
where o.owner_user_id = #{userId}
and o.updated_at > o.created_at
union all
select
(2000000000 + r.id) as id,
'report' as bizType,
r.id as bizId,
case
when r.status = 'reviewed' or lc.score is not null then 'report_reviewed'
else 'report_read'
end as actionType,
case
when r.status = 'reviewed' or lc.score is not null then '日报已点评'
else '日报已阅'
end as title,
case
when lc.score is not null then '主管对你' || to_char(r.report_date, 'MM-DD') || '的日报给出了 ' || lc.score || ' 分'
when r.status = 'reviewed' then '你的' || to_char(r.report_date, 'MM-DD') || '日报已完成主管点评'
else '你的' || to_char(r.report_date, 'MM-DD') || '日报已被查阅'
end as content,
coalesce(lc.reviewer_user_id, r.user_id) as operatorUserId,
coalesce(lc.reviewed_at, r.updated_at, r.created_at) as createdAt
from work_daily_report r
left join latest_report_comment lc on lc.report_id = r.id
where r.user_id = #{userId}
and r.status in ('read', 'reviewed')
union all
select
(3000000000 + c.id) as id,
'channel' as bizType,
c.id as bizId,
'channel_created' as actionType,
'新渠道录入' as title,
'成功录入 ' || c.channel_name || ' 渠道商信息' as content,
c.owner_user_id as operatorUserId,
c.created_at as createdAt
from crm_channel_expansion c
where c.owner_user_id = #{userId}
union all
select
(4000000000 + f.id) as id,
'opportunity_followup' as bizType,
f.opportunity_id as bizId,
'opportunity_followup' as actionType,
'商机跟进新增' as title,
o.opportunity_name || ' 新增了一条' || f.followup_type || '跟进记录' as content,
f.followup_user_id as operatorUserId,
f.followup_time as createdAt
from crm_opportunity_followup f
join crm_opportunity o on o.id = f.opportunity_id
where f.followup_user_id = #{userId}
)
select
a.id,
a.bizType,
a.bizId,
a.actionType,
a.title,
a.content,
a.operatorUserId,
u.display_name as operatorName,
a.createdAt
from activity_union a
left join sys_user u on u.user_id = a.operatorUserId
order by a.createdAt desc nulls last
limit 8
</select>
</mapper>

View File

@ -1,279 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unis.crm.mapper.ExpansionMapper">
<select id="selectDepartments" resultType="com.unis.crm.dto.expansion.DepartmentOptionDTO">
select
id,
org_name as name
from sys_org
where status = 1
order by id asc
</select>
<select id="selectSalesExpansions" resultType="com.unis.crm.dto.expansion.SalesExpansionItemDTO">
select
s.id,
'sales' as type,
s.candidate_name as name,
coalesce(s.mobile, '无') as phone,
coalesce(s.email, '无') as email,
s.target_dept_id as targetDeptId,
'无' as dept,
coalesce(s.industry, '无') as industry,
coalesce(s.title, '无') as title,
s.intent_level as intentLevel,
case s.intent_level
when 'high' then '高'
when 'medium' then '中'
when 'low' then '低'
else '无'
end as intent,
s.stage as stageCode,
case s.stage
when 'initial_contact' then '初步沟通'
when 'solution_discussion' then '方案交流'
when 'bidding' then '招投标'
when 'business_negotiation' then '商务谈判'
when 'won' then '已成交'
when 'lost' then '已放弃'
else coalesce(s.stage, '无')
end as stage,
s.has_desktop_exp as hasExp,
s.in_progress as inProgress,
(s.employment_status = 'active') as active,
s.employment_status as employmentStatus,
coalesce(to_char(s.expected_join_date, 'YYYY-MM-DD'), '无') as expectedJoinDate,
coalesce(s.remark, '无') as notes
from crm_sales_expansion s
where s.owner_user_id = #{userId}
<if test="keyword != null and keyword != ''">
and (
s.candidate_name ilike concat('%', #{keyword}, '%')
or coalesce(s.industry, '') ilike concat('%', #{keyword}, '%')
)
</if>
order by s.updated_at desc, s.id desc
</select>
<select id="selectChannelExpansions" resultType="com.unis.crm.dto.expansion.ChannelExpansionItemDTO">
select
c.id,
'channel' as type,
c.channel_name as name,
coalesce(c.province, '无') as province,
coalesce(c.industry, '无') as industry,
coalesce(cast(c.annual_revenue as varchar), '') as annualRevenue,
case
when c.annual_revenue is null then '无'
when c.annual_revenue >= 10000 then trim(to_char(c.annual_revenue / 10000.0, 'FM999999990.##')) || '万'
else trim(to_char(c.annual_revenue, 'FM999999990.##'))
end as revenue,
coalesce(c.staff_size, 0) as size,
coalesce(c.contact_name, '无') as contact,
coalesce(c.contact_title, '无') as contactTitle,
coalesce(c.contact_mobile, '无') as phone,
c.stage as stageCode,
case c.stage
when 'initial_contact' then '初步接触'
when 'solution_discussion' then '方案交流'
when 'bidding' then '招投标'
when 'business_negotiation' then '合作洽谈'
when 'won' then '已合作'
when 'lost' then '已终止'
else coalesce(c.stage, '无')
end as stage,
c.landed_flag as landed,
coalesce(to_char(c.expected_sign_date, 'YYYY-MM-DD'), '无') as expectedSignDate,
coalesce(c.remark, '无') as notes
from crm_channel_expansion c
where c.owner_user_id = #{userId}
<if test="keyword != null and keyword != ''">
and (
c.channel_name ilike concat('%', #{keyword}, '%')
or coalesce(c.industry, '') ilike concat('%', #{keyword}, '%')
or coalesce(c.province, '') ilike concat('%', #{keyword}, '%')
)
</if>
order by c.updated_at desc, c.id desc
</select>
<select id="selectSalesFollowUps" resultType="com.unis.crm.dto.expansion.ExpansionFollowUpDTO">
select
f.id,
f.biz_id as bizId,
f.biz_type as bizType,
f.followup_time as followUpTime,
f.followup_type as type,
coalesce(f.content, '无') as content,
coalesce(u.display_name, '无') as user
from crm_expansion_followup f
join crm_sales_expansion s on s.id = f.biz_id and f.biz_type = 'sales'
left join sys_user u on u.user_id = f.followup_user_id
where s.owner_user_id = #{userId}
and f.biz_id in
<foreach collection="bizIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
order by f.followup_time desc, f.id desc
</select>
<select id="selectChannelFollowUps" resultType="com.unis.crm.dto.expansion.ExpansionFollowUpDTO">
select
f.id,
f.biz_id as bizId,
f.biz_type as bizType,
f.followup_time as followUpTime,
f.followup_type as type,
coalesce(f.content, '无') as content,
coalesce(u.display_name, '无') as user
from crm_expansion_followup f
join crm_channel_expansion c on c.id = f.biz_id and f.biz_type = 'channel'
left join sys_user u on u.user_id = f.followup_user_id
where c.owner_user_id = #{userId}
and f.biz_id in
<foreach collection="bizIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
order by f.followup_time desc, f.id desc
</select>
<insert id="insertSalesExpansion" useGeneratedKeys="true" keyProperty="request.id">
insert into crm_sales_expansion (
candidate_name,
mobile,
email,
target_dept_id,
industry,
title,
intent_level,
stage,
has_desktop_exp,
in_progress,
employment_status,
expected_join_date,
owner_user_id,
remark
) values (
#{request.candidateName},
#{request.mobile},
#{request.email},
#{request.targetDeptId},
#{request.industry},
#{request.title},
#{request.intentLevel},
#{request.stage},
#{request.hasDesktopExp},
#{request.inProgress},
#{request.employmentStatus},
#{request.expectedJoinDate},
#{userId},
#{request.remark}
)
</insert>
<insert id="insertChannelExpansion" useGeneratedKeys="true" keyProperty="request.id">
insert into crm_channel_expansion (
channel_name,
province,
industry,
annual_revenue,
staff_size,
contact_name,
contact_title,
contact_mobile,
stage,
landed_flag,
expected_sign_date,
owner_user_id,
remark
) values (
#{request.channelName},
#{request.province},
#{request.industry},
#{request.annualRevenue},
#{request.staffSize},
#{request.contactName},
#{request.contactTitle},
#{request.contactMobile},
#{request.stage},
#{request.landedFlag},
#{request.expectedSignDate},
#{userId},
#{request.remark}
)
</insert>
<update id="updateSalesExpansion">
update crm_sales_expansion
set candidate_name = #{request.candidateName},
mobile = #{request.mobile},
email = #{request.email},
target_dept_id = #{request.targetDeptId},
industry = #{request.industry},
title = #{request.title},
intent_level = #{request.intentLevel},
stage = #{request.stage},
has_desktop_exp = #{request.hasDesktopExp},
in_progress = #{request.inProgress},
employment_status = #{request.employmentStatus},
expected_join_date = #{request.expectedJoinDate},
remark = #{request.remark}
where id = #{id}
and owner_user_id = #{userId}
</update>
<update id="updateChannelExpansion">
update crm_channel_expansion
set channel_name = #{request.channelName},
province = #{request.province},
industry = #{request.industry},
annual_revenue = #{request.annualRevenue},
staff_size = #{request.staffSize},
contact_name = #{request.contactName},
contact_title = #{request.contactTitle},
contact_mobile = #{request.contactMobile},
stage = #{request.stage},
landed_flag = #{request.landedFlag},
expected_sign_date = #{request.expectedSignDate},
remark = #{request.remark}
where id = #{id}
and owner_user_id = #{userId}
</update>
<select id="countOwnedSalesExpansion" resultType="int">
select count(1)
from crm_sales_expansion
where id = #{id}
and owner_user_id = #{userId}
</select>
<select id="countOwnedChannelExpansion" resultType="int">
select count(1)
from crm_channel_expansion
where id = #{id}
and owner_user_id = #{userId}
</select>
<insert id="insertExpansionFollowUp">
insert into crm_expansion_followup (
biz_type,
biz_id,
followup_time,
followup_type,
content,
next_action,
followup_user_id
) values (
#{bizType},
#{bizId},
#{request.followUpTime},
#{request.followUpType},
#{request.content},
#{request.nextAction},
#{userId}
)
</insert>
</mapper>

View File

@ -1,197 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unis.crm.mapper.OpportunityMapper">
<select id="selectOpportunities" resultType="com.unis.crm.dto.opportunity.OpportunityItemDTO">
select
o.id,
o.opportunity_code as code,
o.opportunity_name as name,
coalesce(c.customer_name, '未命名客户') as client,
coalesce(u.display_name, '当前用户') as owner,
o.amount,
to_char(o.expected_close_date, 'YYYY-MM-DD') as date,
o.confidence_pct as confidence,
case coalesce(o.stage, 'initial_contact')
when 'initial_contact' then '初步沟通'
when 'solution_discussion' then '方案交流'
when 'bidding' then '招投标'
when 'business_negotiation' then '商务谈判'
when 'won' then '已成交'
when 'lost' then '已放弃'
else coalesce(o.stage, '初步沟通')
end as stage,
coalesce(o.opportunity_type, '新建') as type,
coalesce(o.pushed_to_oms, false) as pushedToOms,
coalesce(o.product_type, 'VDI云桌面') as product,
coalesce(o.source, '主动开发') as source,
coalesce(o.description, '') as notes
from crm_opportunity o
left join crm_customer c on c.id = o.customer_id
left join sys_user u on u.user_id = o.owner_user_id
where o.owner_user_id = #{userId}
<if test="keyword != null and keyword != ''">
and (
o.opportunity_name ilike concat('%', #{keyword}, '%')
or o.opportunity_code ilike concat('%', #{keyword}, '%')
or coalesce(c.customer_name, '') ilike concat('%', #{keyword}, '%')
)
</if>
<if test="stage != null and stage != ''">
and o.stage = #{stage}
</if>
order by coalesce(o.updated_at, o.created_at) desc, o.id desc
</select>
<select id="selectOpportunityFollowUps" resultType="com.unis.crm.dto.opportunity.OpportunityFollowUpDTO">
select
f.id,
f.opportunity_id as opportunityId,
to_char(f.followup_time, 'YYYY-MM-DD HH24:MI') as date,
coalesce(f.followup_type, '无') as type,
coalesce(f.content, '无') as content,
coalesce(u.display_name, '无') as user
from crm_opportunity_followup f
join crm_opportunity o on o.id = f.opportunity_id
left join sys_user u on u.user_id = f.followup_user_id
where o.owner_user_id = #{userId}
and f.opportunity_id in
<foreach collection="opportunityIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
order by f.followup_time desc, f.id desc
</select>
<select id="selectOwnedCustomerIdByName" resultType="java.lang.Long">
select id
from crm_customer
where owner_user_id = #{userId}
and customer_name = #{customerName}
limit 1
</select>
<insert id="insertCustomer">
insert into crm_customer (
id,
customer_code,
customer_name,
owner_user_id,
source,
status,
created_at,
updated_at
) values (
#{id},
'CUS-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad((coalesce((select count(1) from crm_customer), 0) + 1)::text, 3, '0'),
#{customerName},
#{userId},
coalesce(#{source}, '主动开发'),
'following',
now(),
now()
)
</insert>
<insert id="insertOpportunity" useGeneratedKeys="true" keyProperty="request.id">
insert into crm_opportunity (
opportunity_code,
opportunity_name,
customer_id,
owner_user_id,
amount,
expected_close_date,
confidence_pct,
stage,
opportunity_type,
product_type,
source,
pushed_to_oms,
oms_push_time,
description,
status,
created_at,
updated_at
) values (
'OPP-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad((coalesce((select count(1) from crm_opportunity), 0) + 1)::text, 3, '0'),
#{request.opportunityName},
#{customerId},
#{userId},
#{request.amount},
#{request.expectedCloseDate},
#{request.confidencePct},
#{request.stage},
#{request.opportunityType},
#{request.productType},
#{request.source},
#{request.pushedToOms},
case when #{request.pushedToOms} then now() else null end,
#{request.description},
case
when #{request.stage} = 'won' then 'won'
when #{request.stage} = 'lost' then 'lost'
else 'active'
end,
now(),
now()
)
</insert>
<select id="countOwnedOpportunity" resultType="int">
select count(1)
from crm_opportunity
where id = #{id}
and owner_user_id = #{userId}
</select>
<update id="updateOpportunity">
update crm_opportunity
set opportunity_name = #{request.opportunityName},
customer_id = #{customerId},
amount = #{request.amount},
expected_close_date = #{request.expectedCloseDate},
confidence_pct = #{request.confidencePct},
stage = #{request.stage},
opportunity_type = #{request.opportunityType},
product_type = #{request.productType},
source = #{request.source},
pushed_to_oms = #{request.pushedToOms},
oms_push_time = case
when #{request.pushedToOms} then coalesce(oms_push_time, now())
else null
end,
description = #{request.description},
status = case
when #{request.stage} = 'won' then 'won'
when #{request.stage} = 'lost' then 'lost'
else 'active'
end,
updated_at = now()
where id = #{opportunityId}
and owner_user_id = #{userId}
</update>
<insert id="insertOpportunityFollowUp">
insert into crm_opportunity_followup (
opportunity_id,
followup_time,
followup_type,
content,
next_action,
followup_user_id,
created_at,
updated_at
) values (
#{opportunityId},
#{request.followUpTime},
#{request.followUpType},
#{request.content},
#{request.nextAction},
#{userId},
now(),
now()
)
</insert>
</mapper>

View File

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unis.crm.mapper.ProfileMapper">
<select id="selectProfileOverview" resultType="com.unis.crm.dto.profile.ProfileOverviewDTO">
select
u.user_id as userId,
u.display_name as realName,
case
when u.created_at is null then 0
else greatest((current_date - u.created_at::date)::bigint, 0)
end as onboardingDays,
case
when u.status = 1 then '正常'
else '停用'
end as accountStatus
from sys_user u
where u.user_id = #{userId}
and u.is_deleted = 0
limit 1
</select>
<select id="selectUserRoleNames" resultType="java.lang.String">
select r.role_name
from sys_user_role ur
join sys_role r on r.role_id = ur.role_id
where ur.user_id = #{userId}
and ur.is_deleted = 0
and r.is_deleted = 0
order by r.role_id asc
</select>
<select id="selectUserOrgNames" resultType="java.lang.String">
select o.org_name
from sys_tenant_user tu
join sys_org o on o.id = tu.org_id
where tu.user_id = #{userId}
and tu.is_deleted = 0
and o.is_deleted = 0
order by tu.id asc
</select>
<select id="selectMonthlyOpportunityCount" resultType="java.lang.Long">
select count(1)::bigint
from crm_opportunity
where owner_user_id = #{userId}
and date_trunc('month', created_at) = date_trunc('month', now())
</select>
<select id="selectMonthlyExpansionCount" resultType="java.lang.Long">
select (
coalesce((
select count(1)
from crm_sales_expansion
where owner_user_id = #{userId}
and date_trunc('month', created_at) = date_trunc('month', now())
), 0)
+
coalesce((
select count(1)
from crm_channel_expansion
where owner_user_id = #{userId}
and date_trunc('month', created_at) = date_trunc('month', now())
), 0)
)::bigint
</select>
<select id="selectAverageScore" resultType="java.lang.Integer">
with latest_comment as (
select distinct on (c.report_id)
c.report_id,
c.score
from work_daily_report_comment c
order by c.report_id, c.reviewed_at desc nulls last, c.id desc
)
select coalesce(round(avg(coalesce(lc.score, r.score))), 0)::int
from work_daily_report r
left join latest_comment lc on lc.report_id = r.id
where r.user_id = #{userId}
and date_trunc('month', r.report_date::timestamp) = date_trunc('month', now())
</select>
</mapper>

View File

@ -1,374 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unis.crm.mapper.WorkMapper">
<select id="selectTodayCheckIn" resultType="com.unis.crm.dto.work.WorkCheckInDTO">
select
id,
to_char(checkin_date, 'YYYY-MM-DD') as date,
to_char(checkin_time, 'HH24:MI') as time,
coalesce(location_text, '') as locationText,
coalesce(remark, '') as remark,
coalesce(status, 'normal') as status,
longitude,
latitude
from work_checkin
where user_id = #{userId}
and checkin_date = current_date
order by checkin_time desc nulls last, id desc
limit 1
</select>
<select id="selectTodayReport" resultType="com.unis.crm.dto.work.WorkDailyReportDTO">
select
r.id,
to_char(r.report_date, 'YYYY-MM-DD') as date,
to_char(r.submit_time, 'YYYY-MM-DD HH24:MI') as submitTime,
coalesce(r.work_content, '') as workContent,
coalesce(r.tomorrow_plan, '') as tomorrowPlan,
coalesce(r.source_type, 'manual') as sourceType,
coalesce(r.status, 'submitted') as status,
c.score,
c.comment_content as comment
from work_daily_report r
left join (
select distinct on (report_id)
report_id,
score,
comment_content
from work_daily_report_comment
order by report_id, reviewed_at desc nulls last, id desc
) c on c.report_id = r.id
where r.user_id = #{userId}
and r.report_date = current_date
order by r.submit_time desc nulls last, r.id desc
limit 1
</select>
<select id="selectTodayWorkContentActions" resultType="com.unis.crm.dto.work.WorkSuggestedActionDTO">
select
group_name as groupName,
detail
from (
select
coalesce(s.created_at, now()) as action_time,
coalesce(s.candidate_name, '销售拓展') as group_name,
'新增销售拓展' ||
case
when s.title is not null and btrim(s.title) &lt;&gt; '' then ',岗位:' || s.title
else ''
end ||
case
when s.intent_level is not null and btrim(s.intent_level) &lt;&gt; '' then ',意向:' || s.intent_level
else ''
end as detail
from crm_sales_expansion s
where s.owner_user_id = #{userId}
and s.created_at::date = current_date
union all
select
coalesce(c.created_at, now()) as action_time,
coalesce(c.channel_name, '渠道拓展') as group_name,
'新增渠道拓展' ||
case
when c.province is not null and btrim(c.province) &lt;&gt; '' then ',地区:' || c.province
else ''
end ||
case
when c.industry is not null and btrim(c.industry) &lt;&gt; '' then ',行业:' || c.industry
else ''
end as detail
from crm_channel_expansion c
where c.owner_user_id = #{userId}
and c.created_at::date = current_date
union all
select
coalesce(o.created_at, now()) as action_time,
coalesce(nullif(btrim(cust.customer_name), ''), nullif(btrim(o.opportunity_name), ''), '商机客户') as group_name,
'新增商机:' ||
coalesce(o.opportunity_name, '未命名商机') ||
case
when o.amount is not null then ',金额:¥' || trim(to_char(o.amount, 'FM9999999999990.00'))
else ''
end as detail
from crm_opportunity o
left join crm_customer cust on cust.id = o.customer_id
where o.owner_user_id = #{userId}
and o.created_at::date = current_date
union all
select
coalesce(f.followup_time, now()) as action_time,
coalesce(s.candidate_name, '销售拓展') as group_name,
'销售拓展跟进' ||
case
when f.followup_type is not null and btrim(f.followup_type) &lt;&gt; '' then ',方式:' || f.followup_type
else ''
end ||
case
when f.content is not null and btrim(f.content) &lt;&gt; '' then ',内容:' || f.content
else ''
end as detail
from crm_expansion_followup f
join crm_sales_expansion s on s.id = f.biz_id and f.biz_type = 'sales'
where f.followup_user_id = #{userId}
and f.followup_time::date = current_date
union all
select
coalesce(f.followup_time, now()) as action_time,
coalesce(c.channel_name, '渠道拓展') as group_name,
'渠道拓展跟进' ||
case
when f.followup_type is not null and btrim(f.followup_type) &lt;&gt; '' then ',方式:' || f.followup_type
else ''
end ||
case
when f.content is not null and btrim(f.content) &lt;&gt; '' then ',内容:' || f.content
else ''
end as detail
from crm_expansion_followup f
join crm_channel_expansion c on c.id = f.biz_id and f.biz_type = 'channel'
where f.followup_user_id = #{userId}
and f.followup_time::date = current_date
union all
select
coalesce(f.followup_time, now()) as action_time,
coalesce(nullif(btrim(cust.customer_name), ''), nullif(btrim(o.opportunity_name), ''), '商机客户') as group_name,
'商机跟进' ||
case
when f.followup_type is not null and btrim(f.followup_type) &lt;&gt; '' then ',方式:' || f.followup_type
else ''
end ||
case
when f.content is not null and btrim(f.content) &lt;&gt; '' then ',内容:' || f.content
else ''
end as detail
from crm_opportunity_followup f
join crm_opportunity o on o.id = f.opportunity_id
left join crm_customer cust on cust.id = o.customer_id
where f.followup_user_id = #{userId}
and f.followup_time::date = current_date
) work_lines
order by action_time asc, group_name asc, detail asc
</select>
<select id="selectHistory" resultType="com.unis.crm.dto.work.WorkHistoryItemDTO">
select
id,
type,
date,
time,
content,
status,
score,
comment
from (
select
c.id,
'外勤打卡' as type,
to_char(c.checkin_date, 'YYYY-MM-DD') as date,
to_char(c.checkin_time, 'HH24:MI') as time,
coalesce(c.location_text, '') ||
case
when c.remark is not null and btrim(c.remark) &lt;&gt; '' then E'\n备注' || c.remark
else ''
end as content,
case coalesce(c.status, 'normal')
when 'normal' then '正常'
when 'updated' then '已更新'
else coalesce(c.status, '正常')
end as status,
null::integer as score,
null::text as comment,
coalesce(c.checkin_date::timestamp + c.checkin_time::time, c.created_at) as sort_time
from work_checkin c
where c.user_id = #{userId}
union all
select
r.id,
'日报' as type,
to_char(r.report_date, 'YYYY-MM-DD') as date,
to_char(r.submit_time, 'HH24:MI') as time,
coalesce(r.work_content, '') ||
case
when r.tomorrow_plan is not null and btrim(r.tomorrow_plan) &lt;&gt; '' then E'\n明日计划' || r.tomorrow_plan
else ''
end as content,
case coalesce(rc.comment_content, '')
when '' then
case coalesce(r.status, 'submitted')
when 'submitted' then '已提交'
when 'reviewed' then '已点评'
else coalesce(r.status, '已提交')
end
else '已点评'
end as status,
rc.score,
rc.comment_content as comment,
coalesce(r.report_date::timestamp + r.submit_time::time, r.created_at) as sort_time
from work_daily_report r
left join (
select distinct on (report_id)
report_id,
score,
comment_content
from work_daily_report_comment
order by report_id, reviewed_at desc nulls last, id desc
) rc on rc.report_id = r.id
where r.user_id = #{userId}
) history
order by sort_time desc nulls last, id desc
</select>
<select id="selectTodayCheckInId" resultType="java.lang.Long">
select id
from work_checkin
where user_id = #{userId}
and checkin_date = current_date
order by checkin_time desc nulls last, id desc
limit 1
</select>
<insert id="insertCheckIn">
insert into work_checkin (
id,
user_id,
checkin_date,
checkin_time,
longitude,
latitude,
location_text,
remark,
status,
created_at,
updated_at
) values (
(select coalesce(max(id), 0) + 1 from work_checkin),
#{userId},
current_date,
now(),
#{request.longitude},
#{request.latitude},
#{request.locationText},
#{request.remark},
'normal',
now(),
now()
)
</insert>
<update id="updateCheckIn">
update work_checkin
set checkin_time = now(),
longitude = #{request.longitude},
latitude = #{request.latitude},
location_text = #{request.locationText},
remark = #{request.remark},
status = 'normal',
updated_at = now()
where id = #{checkInId}
</update>
<select id="selectTodayReportId" resultType="java.lang.Long">
select id
from work_daily_report
where user_id = #{userId}
and report_date = current_date
order by submit_time desc nulls last, id desc
limit 1
</select>
<insert id="insertDailyReport">
insert into work_daily_report (
user_id,
report_date,
work_content,
tomorrow_plan,
source_type,
submit_time,
status,
created_at,
updated_at
) values (
#{userId},
current_date,
#{request.workContent},
#{request.tomorrowPlan},
#{request.sourceType},
now(),
'submitted',
now(),
now()
)
</insert>
<update id="updateDailyReport">
update work_daily_report
set work_content = #{request.workContent},
tomorrow_plan = #{request.tomorrowPlan},
source_type = #{request.sourceType},
submit_time = now(),
status = 'submitted',
updated_at = now()
where id = #{reportId}
</update>
<select id="selectTodoIdByBiz" resultType="java.lang.Long">
select id
from work_todo
where user_id = #{userId}
and biz_type = #{bizType}
and biz_id = #{bizId}
limit 1
</select>
<insert id="insertTodo">
insert into work_todo (
id,
user_id,
title,
biz_type,
biz_id,
due_date,
status,
priority,
created_at,
updated_at
) values (
#{todoId},
#{userId},
#{title},
#{bizType},
#{bizId},
current_date::timestamp + interval '1 day' + time '09:00',
'todo',
'medium',
now(),
now()
)
</insert>
<update id="updateTodo">
update work_todo
set title = #{title},
due_date = current_date::timestamp + interval '1 day' + time '09:00',
status = 'todo',
priority = 'medium',
updated_at = now()
where id = #{todoId}
</update>
</mapper>

View File

@ -1,3 +0,0 @@
artifactId=unis-crm-backend
groupId=com.unis.crm
version=1.0.0-SNAPSHOT

View File

@ -1,36 +0,0 @@
com/unis/crm/service/impl/WorkServiceImpl$PhotoMetadata.class
com/unis/crm/service/impl/ExpansionServiceImpl.class
com/unis/crm/dto/dashboard/UserWelcomeDTO.class
com/unis/crm/dto/expansion/ExpansionOverviewDTO.class
com/unis/crm/service/impl/DashboardServiceImpl.class
com/unis/crm/controller/ExpansionController.class
com/unis/crm/dto/expansion/ExpansionFollowUpDTO.class
com/unis/crm/dto/expansion/CreateSalesExpansionRequest.class
com/unis/crm/common/ApiResponse.class
com/unis/crm/UnisCrmBackendApplication.class
com/unis/crm/common/CurrentUserUtils.class
com/unis/crm/service/DashboardService.class
com/unis/crm/dto/expansion/UpdateChannelExpansionRequest.class
com/unis/crm/common/CrmGlobalExceptionHandler.class
com/unis/crm/dto/dashboard/DashboardStatDTO.class
com/unis/crm/dto/work/WorkSuggestedActionDTO.class
com/unis/crm/dto/expansion/CreateChannelExpansionRequest.class
com/unis/crm/dto/expansion/DepartmentOptionDTO.class
com/unis/crm/dto/expansion/SalesExpansionItemDTO.class
com/unis/crm/dto/dashboard/DashboardActivityDTO.class
com/unis/crm/service/ExpansionService.class
com/unis/crm/dto/expansion/UpdateSalesExpansionRequest.class
com/unis/crm/dto/profile/ProfileOverviewDTO.class
com/unis/crm/dto/dashboard/DashboardTodoDTO.class
com/unis/crm/controller/DashboardController.class
com/unis/crm/controller/ProfileController.class
com/unis/crm/dto/expansion/ExpansionMetaDTO.class
com/unis/crm/service/impl/ProfileServiceImpl.class
com/unis/crm/mapper/ExpansionMapper.class
com/unis/crm/dto/dashboard/DashboardHomeDTO.class
com/unis/crm/mapper/ProfileMapper.class
com/unis/crm/service/ProfileService.class
com/unis/crm/common/BusinessException.class
com/unis/crm/mapper/DashboardMapper.class
com/unis/crm/dto/expansion/CreateExpansionFollowUpRequest.class
com/unis/crm/dto/expansion/ChannelExpansionItemDTO.class

View File

@ -1,54 +0,0 @@
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/mapper/ExpansionMapper.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/WorkDailyReportDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardHomeDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityFollowUpDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/opportunity/CreateOpportunityFollowUpRequest.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionOverviewDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/CreateWorkCheckInRequest.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/common/CurrentUserUtils.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/controller/DashboardController.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/common/ApiResponse.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/WorkServiceImpl.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/UnisCrmBackendApplication.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/DepartmentOptionDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionMetaDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardTodoDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/WorkHistoryItemDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/CreateWorkDailyReportRequest.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/WorkService.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/profile/ProfileOverviewDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/WorkCheckInDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/controller/OpportunityController.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/WorkOverviewDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/WorkSuggestedActionDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/dashboard/UserWelcomeDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityItemDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardActivityDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/ProfileService.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/mapper/DashboardMapper.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardStatDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/DashboardService.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/OpportunityService.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/controller/WorkController.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/CreateSalesExpansionRequest.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/controller/ExpansionController.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/common/BusinessException.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/DashboardServiceImpl.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/ExpansionService.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/UpdateChannelExpansionRequest.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionFollowUpDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityOverviewDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/mapper/WorkMapper.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/UpdateSalesExpansionRequest.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/ChannelExpansionItemDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/opportunity/CreateOpportunityRequest.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/CreateChannelExpansionRequest.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/SalesExpansionItemDTO.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/ProfileServiceImpl.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/CreateExpansionFollowUpRequest.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/controller/ProfileController.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/mapper/ProfileMapper.java
/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/common/CrmGlobalExceptionHandler.java

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,22 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="18" fill="url(#bg)"/>
<rect x="4" y="4" width="56" height="56" rx="14" stroke="rgba(255,255,255,0.22)"/>
<text
x="32"
y="38"
text-anchor="middle"
font-size="21"
font-weight="700"
font-family="Arial, PingFang SC, Microsoft YaHei, sans-serif"
fill="white"
letter-spacing="0.5"
>
CRM
</text>
<defs>
<linearGradient id="bg" x1="10" y1="8" x2="56" y2="56" gradientUnits="userSpaceOnUse">
<stop stop-color="#7C3AED"/>
<stop offset="1" stop-color="#4F46E5"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 684 B

View File

@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/crm-favicon.svg" />
<title>紫光汇智CRM</title>
<script type="module" crossorigin src="/assets/index-Ba78XVP4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D3WIva4A.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -524,11 +524,140 @@ export async function getWorkOverview() {
} }
export async function reverseWorkGeocode(latitude: number, longitude: number) { export async function reverseWorkGeocode(latitude: number, longitude: number) {
const params = new URLSearchParams({ const nominatimParams = new URLSearchParams({
format: "jsonv2",
addressdetails: "1",
namedetails: "1",
extratags: "1",
zoom: "19",
lat: String(latitude), lat: String(latitude),
lon: String(longitude), lon: String(longitude),
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
}); });
return request<string>(`/api/work/reverse-geocode?${params.toString()}`, undefined, true);
try {
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?${nominatimParams.toString()}`, {
headers: {
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`Nominatim reverse geocoding failed (${response.status})`);
}
const payload = (await response.json()) as {
display_name?: string;
address?: Record<string, string>;
namedetails?: Record<string, string>;
};
const orderedLocation = buildNominatimLocationName(payload);
if (orderedLocation) {
return orderedLocation;
}
} catch {
// Fall through to the browser-side free fallback.
}
const bigDataCloudParams = new URLSearchParams({
latitude: String(latitude),
longitude: String(longitude),
localityLanguage: "zh",
});
const fallbackResponse = await fetch(`https://api.bigdatacloud.net/data/reverse-geocode-client?${bigDataCloudParams.toString()}`);
if (!fallbackResponse.ok) {
throw new Error("地点解析失败,请稍后重试");
}
const fallbackPayload = (await fallbackResponse.json()) as {
locality?: string;
city?: string;
principalSubdivision?: string;
countryName?: string;
};
const fallbackLocation = joinLocationParts(
fallbackPayload.countryName,
fallbackPayload.principalSubdivision,
fallbackPayload.city,
fallbackPayload.locality,
);
if (fallbackLocation) {
return fallbackLocation;
}
throw new Error("未能解析出具体地点名称");
}
function buildNominatimLocationName(payload: {
display_name?: string;
address?: Record<string, string>;
namedetails?: Record<string, string>;
}) {
const address = payload.address ?? {};
const namedetails = payload.namedetails ?? {};
const regionPart = joinLocationParts(
firstDefined(address.state, address.province, address.region),
firstDefined(address.city, address.municipality, address.town, address.county),
firstDefined(address.district, address.city_district, address.borough),
);
const streetPart = joinLocationParts(
firstDefined(address.suburb, address.township, address.quarter, address.neighbourhood),
firstDefined(address.road, address.street, address.pedestrian),
joinLocationParts(address.house_number, address.house_name),
);
const buildingPart = firstDefined(
address.building,
address.city_block,
address.amenity,
address.office,
address.shop,
address.commercial,
address.residential,
address.industrial,
address.retail,
namedetails.name,
namedetails.official_name,
namedetails.short_name,
);
return firstDefined(
joinLocationParts(regionPart, streetPart, buildingPart),
joinLocationParts(regionPart, streetPart),
normalizeLocationText(payload.display_name),
);
}
function firstDefined(...values: Array<string | undefined>) {
for (const value of values) {
const normalized = normalizeLocationText(value);
if (normalized) {
return normalized;
}
}
return undefined;
}
function joinLocationParts(...values: Array<string | undefined>) {
const result: string[] = [];
for (const value of values) {
const normalized = normalizeLocationText(value);
if (!normalized || result.includes(normalized)) {
continue;
}
result.push(normalized);
}
return result.length ? result.join("") : undefined;
}
function normalizeLocationText(value?: string) {
const normalized = value?.trim();
return normalized ? normalized : undefined;
} }
export async function saveWorkCheckIn(payload: CreateWorkCheckInPayload) { export async function saveWorkCheckIn(payload: CreateWorkCheckInPayload) {

View File

@ -103,7 +103,7 @@ export default function Work() {
const displayName = await reverseWorkGeocode(latitude, longitude); const displayName = await reverseWorkGeocode(latitude, longitude);
setCheckInForm((prev) => ({ setCheckInForm((prev) => ({
...prev, ...prev,
locationText: displayName || `定位坐标:${latitude}, ${longitude}`, locationText: displayName || formatLocationFallback(latitude, longitude),
latitude, latitude,
longitude, longitude,
})); }));
@ -111,15 +111,15 @@ export default function Work() {
setLocationHint(displayName setLocationHint(displayName
? "定位已刷新并锁定当前位置,如需变更请点击“刷新定位”。" ? "定位已刷新并锁定当前位置,如需变更请点击“刷新定位”。"
: "已获取定位坐标,如需更精确地址可再次刷新定位。"); : "已获取定位坐标,如需更精确地址可再次刷新定位。");
} catch { } catch (error) {
setCheckInForm((prev) => ({ setCheckInForm((prev) => ({
...prev, ...prev,
locationText: `定位坐标:${latitude}, ${longitude}`, locationText: formatLocationFallback(latitude, longitude),
latitude, latitude,
longitude, longitude,
})); }));
setLocationLocked(false); setLocationLocked(false);
setLocationHint("已获取坐标,但地点名称解析失败,你也可以手动补充。"); setLocationHint(getReverseGeocodeHint(error));
} }
} catch (error) { } catch (error) {
setLocationLocked(false); setLocationLocked(false);
@ -533,6 +533,18 @@ export default function Work() {
); );
} }
function formatLocationFallback(latitude: number, longitude: number) {
return `当前位置待补充(坐标:${latitude}, ${longitude}`;
}
function getReverseGeocodeHint(error: unknown) {
const message = error instanceof Error ? error.message : "";
if (message.includes("地点解析失败")) {
return "已获取定位坐标,但地址解析服务暂时不可用,你也可以手动补充当前位置。";
}
return "已获取坐标,但地点名称解析失败,你也可以手动补充。";
}
function getGeoErrorMessage(error: GeolocationPositionError) { function getGeoErrorMessage(error: GeolocationPositionError) {
if (!window.isSecureContext) { if (!window.isSecureContext) {
return "手机端定位需要通过安全地址访问。请使用 HTTPS或继续手动填写当前位置。"; return "手机端定位需要通过安全地址访问。请使用 HTTPS或继续手动填写当前位置。";

View File

@ -0,0 +1,45 @@
-- sys_user: migrate dept_id -> org_id (idempotent)
-- Target state:
-- 1) sys_user has org_id column
-- 2) dept_id column removed
-- 3) idx_sys_user_org_id exists
begin;
do $$
begin
if to_regclass('public.sys_user') is not null then
-- If org_id is missing, add it.
if not exists (
select 1
from information_schema.columns
where table_schema = 'public'
and table_name = 'sys_user'
and column_name = 'org_id'
) then
execute 'alter table public.sys_user add column org_id bigint';
end if;
-- If dept_id exists, backfill org_id and then drop dept_id.
if exists (
select 1
from information_schema.columns
where table_schema = 'public'
and table_name = 'sys_user'
and column_name = 'dept_id'
) then
execute 'update public.sys_user set org_id = coalesce(org_id, dept_id) where dept_id is not null';
execute 'alter table public.sys_user drop column dept_id';
end if;
end if;
end $$;
do $$
begin
if to_regclass('public.sys_user') is not null then
execute 'drop index if exists public.idx_sys_user_dept_id';
execute 'create index if not exists idx_sys_user_org_id on public.sys_user(org_id)';
end if;
end $$;
commit;

View File

@ -40,18 +40,6 @@ begin
end; end;
$$; $$;
create table if not exists sys_department (
id bigint generated by default as identity primary key,
dept_code varchar(50),
dept_name varchar(100) not null,
parent_id bigint,
manager_user_id bigint,
status smallint not null default 1,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uk_sys_department_code unique (dept_code)
);
create table if not exists sys_user ( create table if not exists sys_user (
id bigint generated by default as identity primary key, id bigint generated by default as identity primary key,
user_code varchar(50), user_code varchar(50),
@ -59,7 +47,7 @@ create table if not exists sys_user (
real_name varchar(50) not null, real_name varchar(50) not null,
mobile varchar(20), mobile varchar(20),
email varchar(100), email varchar(100),
dept_id bigint, org_id bigint,
job_title varchar(100), job_title varchar(100),
status smallint not null default 1, status smallint not null default 1,
hire_date date, hire_date date,
@ -68,8 +56,7 @@ create table if not exists sys_user (
created_at timestamptz not null default now(), created_at timestamptz not null default now(),
updated_at timestamptz not null default now(), updated_at timestamptz not null default now(),
constraint uk_sys_user_username unique (username), constraint uk_sys_user_username unique (username),
constraint uk_sys_user_mobile unique (mobile), constraint uk_sys_user_mobile unique (mobile)
constraint fk_sys_user_dept foreign key (dept_id) references sys_department(id)
); );
create table if not exists crm_customer ( create table if not exists crm_customer (
@ -206,18 +193,6 @@ create table if not exists work_checkin (
updated_at timestamptz not null default now() updated_at timestamptz not null default now()
); );
create table if not exists work_checkin_attachment (
id bigint generated by default as identity primary key,
checkin_id bigint not null,
file_url varchar(255) not null,
file_type varchar(30) not null check (file_type in ('image', 'audio', 'video')),
file_name varchar(255),
file_size bigint check (file_size is null or file_size >= 0),
created_at timestamptz not null default now(),
constraint fk_work_checkin_attachment_checkin
foreign key (checkin_id) references work_checkin(id) on delete cascade
);
create table if not exists work_daily_report ( create table if not exists work_daily_report (
id bigint generated by default as identity primary key, id bigint generated by default as identity primary key,
user_id bigint not null, user_id bigint not null,
@ -276,7 +251,7 @@ create table if not exists sys_activity_log (
create index if not exists idx_crm_customer_owner on crm_customer(owner_user_id); create index if not exists idx_crm_customer_owner on crm_customer(owner_user_id);
create index if not exists idx_crm_customer_name on crm_customer(customer_name); create index if not exists idx_crm_customer_name on crm_customer(customer_name);
create index if not exists idx_sys_user_dept_id on sys_user(dept_id); create index if not exists idx_sys_user_org_id on sys_user(org_id);
create index if not exists idx_crm_opportunity_customer on crm_opportunity(customer_id); create index if not exists idx_crm_opportunity_customer on crm_opportunity(customer_id);
create index if not exists idx_crm_opportunity_owner on crm_opportunity(owner_user_id); create index if not exists idx_crm_opportunity_owner on crm_opportunity(owner_user_id);
create index if not exists idx_crm_opportunity_stage on crm_opportunity(stage); create index if not exists idx_crm_opportunity_stage on crm_opportunity(stage);
@ -294,14 +269,12 @@ create index if not exists idx_crm_expansion_followup_biz_time
on crm_expansion_followup(biz_type, biz_id, followup_time desc); on crm_expansion_followup(biz_type, biz_id, followup_time desc);
create index if not exists idx_crm_expansion_followup_user on crm_expansion_followup(followup_user_id); create index if not exists idx_crm_expansion_followup_user on crm_expansion_followup(followup_user_id);
create index if not exists idx_work_checkin_user_date on work_checkin(user_id, checkin_date desc); create index if not exists idx_work_checkin_user_date on work_checkin(user_id, checkin_date desc);
create index if not exists idx_work_checkin_attachment_checkin on work_checkin_attachment(checkin_id);
create index if not exists idx_work_daily_report_user_date on work_daily_report(user_id, report_date desc); create index if not exists idx_work_daily_report_user_date on work_daily_report(user_id, report_date desc);
create index if not exists idx_work_daily_report_status on work_daily_report(status); create index if not exists idx_work_daily_report_status on work_daily_report(status);
create index if not exists idx_work_daily_report_comment_report on work_daily_report_comment(report_id); create index if not exists idx_work_daily_report_comment_report on work_daily_report_comment(report_id);
create index if not exists idx_sys_activity_log_created on sys_activity_log(created_at desc); create index if not exists idx_sys_activity_log_created on sys_activity_log(created_at desc);
create index if not exists idx_sys_activity_log_biz on sys_activity_log(biz_type, biz_id); create index if not exists idx_sys_activity_log_biz on sys_activity_log(biz_type, biz_id);
select create_trigger_if_not_exists('trg_sys_department_updated_at', 'sys_department');
select create_trigger_if_not_exists('trg_sys_user_updated_at', 'sys_user'); select create_trigger_if_not_exists('trg_sys_user_updated_at', 'sys_user');
select create_trigger_if_not_exists('trg_crm_customer_updated_at', 'crm_customer'); select create_trigger_if_not_exists('trg_crm_customer_updated_at', 'crm_customer');
select create_trigger_if_not_exists('trg_crm_opportunity_updated_at', 'crm_opportunity'); select create_trigger_if_not_exists('trg_crm_opportunity_updated_at', 'crm_opportunity');
@ -313,7 +286,6 @@ select create_trigger_if_not_exists('trg_work_checkin_updated_at', 'work_checkin
select create_trigger_if_not_exists('trg_work_daily_report_updated_at', 'work_daily_report'); select create_trigger_if_not_exists('trg_work_daily_report_updated_at', 'work_daily_report');
select create_trigger_if_not_exists('trg_work_todo_updated_at', 'work_todo'); select create_trigger_if_not_exists('trg_work_todo_updated_at', 'work_todo');
comment on table sys_department is '组织部门';
comment on table sys_user is '系统用户'; comment on table sys_user is '系统用户';
comment on table crm_customer is '客户主表'; comment on table crm_customer is '客户主表';
comment on table crm_opportunity is '商机主表'; comment on table crm_opportunity is '商机主表';