myPayslip.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import request from '@/utils/request';
  2. /**
  3. * 员工自助 · 我的工资条
  4. *
  5. * 后端分组:人力资源-员工自助明细服务(/hr/ess/**),全部按当前登录人鉴权,
  6. * 接口不接收 userId / 员工主键(后端以登录态复核归属)。
  7. *
  8. * 接口契约(2026-10-30 后端改造后):
  9. * 1. POST /hr/ess/payslips/page —— 分页查询我的工资条(**新接入优先使用**)
  10. * body 可空:{ pageNum, size };size 超过 100 由服务端收敛;返回 Pagination。
  11. * 只返回当前有效版本(更正后旧版本不再出现),行内带 confirmTime。
  12. * 2. GET /hr/ess/payslips —— 兼容保留的全量列表(老调用方仍在用),结构不变。
  13. * 3. GET /hr/ess/payslips/{id}?verificationGrantId= —— 查询我的工资条详情
  14. * ★ verificationGrantId 改为**选填**:不传直接返回本人明文金额;
  15. * 传了必须是本人、本工资条、未过期的有效授权,否则仍被拒绝。
  16. * 4. POST /hr/ess/payslips/{id}/confirm?requestId=&verificationGrantId=
  17. * 参数在 query 上(不是 body),requestId 必填并幂等重放;grantId 选填。
  18. * 5. POST /hr/ess/payslips/{id}/appeals —— 提交异议
  19. * body:appealReason(≤500) + requestId(≤128) 必填,verificationGrantId 选填。
  20. *
  21. * ★ 状态文案:后端枚举 PayrollPayslipStatusEnum 统一在 payslipStatusLabel 字段下发中文,
  22. * 前端不再维护状态字典、也不再请求 sys_dict;下面的内置文案只作为旧后端兜底。
  23. * ★ 安全口径:二次认证授权降级为「有则校验」后,服务端对未携带授权的详情访问
  24. * 单独记一条 PAYSLIP_VIEW_NO_SECOND_FACTOR 审计;前端不伪造、不猜授权值。
  25. */
  26. import {
  27. PAYSLIP_STATUS_OPTIONS,
  28. createIdempotentKey,
  29. isIdempotentKeyExpired
  30. } from './salarySlip';
  31. /** 旧后端兜底文案:与 PayrollConstants 的 7 个码值一致,正常不再使用 */
  32. const FALLBACK_STATUS_LABELS = {
  33. WAIT_PUBLISH: '待发布',
  34. PUBLISHED: '已发布',
  35. VIEWED: '已查看',
  36. CONFIRMED: '已确认',
  37. APPEALED: '申诉中',
  38. CORRECTED: '已更正',
  39. CLOSED: '已关闭'
  40. };
  41. /** 状态码 → 文案:优先后端下发的 payslipStatusLabel,其次兜底表,最后原样显示 */
  42. function payslipStatusLabel(raw = {}, status = '') {
  43. const code = String(status || '').toUpperCase();
  44. return (
  45. String(raw.payslipStatusLabel || '').trim() ||
  46. FALLBACK_STATUS_LABELS[code] ||
  47. code ||
  48. '—'
  49. );
  50. }
  51. export { createIdempotentKey, isIdempotentKeyExpired };
  52. function unwrap(res) {
  53. if (res.data.code == 0) {
  54. return res.data.data;
  55. }
  56. return Promise.reject(new Error(res.data.message || '接口调用失败'));
  57. }
  58. function toId(value) {
  59. if (value === undefined || value === null || value === '') return undefined;
  60. const text = String(value).trim();
  61. if (!/^-?\d+$/.test(text)) return value;
  62. return Number.isSafeInteger(Number(text)) ? Number(text) : text;
  63. }
  64. /* ---------------- 二次认证授权(后端缺口,唯一取数口) ---------------- */
  65. /**
  66. * 获取「可信二次认证授权主键」(verificationGrantId)。
  67. *
  68. * ★ 2026-10-30 契约降级:grantId 改为选填,后端不再要求必须先取得授权,
  69. * 因此这里直接返回 undefined(请求层会省略该参数),不再抛错阻断用户。
  70. * 若后续补上发放接口,只改这一个函数即可(详情 / 确认 / 申诉共用)。
  71. *
  72. * @param {string} _scene 业务场景(detail / confirm / appeal,预留给授权发放接口区分用途)
  73. * @returns {Promise<number|undefined>} verificationGrantId(int64),未取得授权时为 undefined
  74. */
  75. export async function resolveVerificationGrantId(_scene = '') {
  76. return undefined;
  77. }
  78. /* ---------------- 适配器 ---------------- */
  79. /**
  80. * 员工自助工资条行适配。
  81. * 契约字段:payslipId / payrollMonth / payslipStatus / payslipStatusLabel / confirmTime /
  82. * grossAmount / deductionAmount / netAmount / maskedBankCardNo /
  83. * publishTime / firstViewTime / appealReason
  84. * ★ 状态中文文案由后端枚举 PayrollPayslipStatusEnum 在 payslipStatusLabel 下发,
  85. * 前端只在旧后端缺该字段时用兜底表,不猜测语义。
  86. * ★ 金额字段列表接口不返回(null)→ 原样保留 undefined,页面显示「—」并引导去详情,不做兜底。
  87. */
  88. function adaptMyPayslipRow(raw = {}) {
  89. const status = String(raw.payslipStatus || '').toUpperCase();
  90. return {
  91. ...raw,
  92. /* 列表行主键叫 payslipId(不是 id),统一透出 id 便于表格 row-key */
  93. id: raw.payslipId,
  94. payslipId: toId(raw.payslipId),
  95. payrollMonth: raw.payrollMonth || '',
  96. payslipStatus: status,
  97. payslipStatusLabel: payslipStatusLabel(raw, status),
  98. grossAmount: raw.grossAmount,
  99. deductionAmount: raw.deductionAmount,
  100. netAmount: raw.netAmount,
  101. maskedBankCardNo: raw.maskedBankCardNo || '',
  102. publishTime: raw.publishTime || '',
  103. firstViewTime: raw.firstViewTime || '',
  104. confirmTime: raw.confirmTime || '',
  105. appealReason: raw.appealReason || ''
  106. };
  107. }
  108. /** 状态筛选项(与薪资核算工资条共用同一套枚举文案) */
  109. export const MY_PAYSLIP_STATUS_OPTIONS = PAYSLIP_STATUS_OPTIONS;
  110. /** 金额是否真实返回(0 是合法金额,不能用 falsy 判断) */
  111. export function hasAmount(value) {
  112. return typeof value === 'number' && !Number.isNaN(value);
  113. }
  114. /* ---------------- 接口 ---------------- */
  115. /**
  116. * 分页查询我的工资条(新接入优先使用)
  117. * POST /hr/ess/payslips/page
  118. * @param {object} params { pageNum, size }
  119. * @returns {Promise<{list: Array, count: number, pageNum: number, size: number}>}
  120. */
  121. export async function getMyPayslipPage(params = {}) {
  122. const pageNum = Number(params.pageNum) > 0 ? Number(params.pageNum) : 1;
  123. const size = Number(params.size) > 0 ? Math.min(Number(params.size), 100) : 20;
  124. const res = await request.post('/hr/ess/payslips/page', { pageNum, size });
  125. const data = unwrap(res) || {};
  126. const list = (Array.isArray(data.list) ? data.list : []).map(
  127. adaptMyPayslipRow
  128. );
  129. const count = Number(data.count != null ? data.count : list.length) || 0;
  130. return { list, count, pageNum: data.pageNum || pageNum, size: data.size || size };
  131. }
  132. /**
  133. * 查询我的工资条列表(兼容保留)
  134. * GET /hr/ess/payslips(无参数,非分页,后端按登录人返回全量)
  135. * ★ 老调用方仍可用;需要分页/确认时间/仅当前有效版本时改用 getMyPayslipPage。
  136. */
  137. export async function getMyPayslipList() {
  138. const res = await request.get('/hr/ess/payslips');
  139. const list = (Array.isArray(unwrap(res)) ? unwrap(res) : []).map(
  140. adaptMyPayslipRow
  141. );
  142. return { list, count: list.length };
  143. }
  144. /**
  145. * 查询我的工资条详情(明文金额)
  146. * GET /hr/ess/payslips/{id}?verificationGrantId=
  147. * @param {number|string} payslipId 工资条主键
  148. * @param {number|string} [verificationGrantId] 二次认证授权主键(选填;有则带上,后端会校验有效性)
  149. */
  150. export async function getMyPayslipDetail(payslipId, verificationGrantId) {
  151. const grantId = toId(verificationGrantId);
  152. const res = await request.get(`/hr/ess/payslips/${toId(payslipId)}`, {
  153. params: grantId == null ? {} : { verificationGrantId: grantId }
  154. });
  155. return adaptMyPayslipRow(unwrap(res) || {});
  156. }
  157. /**
  158. * 确认我的工资条
  159. * POST /hr/ess/payslips/{id}/confirm?requestId=&verificationGrantId=
  160. * ★ 参数在 query 上;同一 requestId 幂等重放 —— requestId 必须由调用方生成一次并复用
  161. * (createIdempotentKey + isIdempotentKeyExpired,10 分钟内重试复用同一个)。
  162. */
  163. export async function confirmMyPayslip(payslipId, payload = {}) {
  164. const requestId = String(payload.requestId || '').trim();
  165. if (!requestId) {
  166. throw new Error(
  167. '缺少客户端请求号(requestId)。请用 createIdempotentKey() 生成一次并在重试时复用'
  168. );
  169. }
  170. const grantId = toId(payload.verificationGrantId);
  171. const params = grantId == null ? { requestId } : { requestId, verificationGrantId: grantId };
  172. const res = await request.post(
  173. `/hr/ess/payslips/${toId(payslipId)}/confirm`,
  174. null,
  175. { params }
  176. );
  177. return unwrap(res);
  178. }
  179. /**
  180. * 提交我的工资条异议
  181. * POST /hr/ess/payslips/{id}/appeals
  182. * body:{ appealReason(≤500,必填), requestId(≤128,必填), verificationGrantId(选填) }
  183. */
  184. export async function submitMyPayslipAppeal(payslipId, payload = {}) {
  185. const appealReason = String(payload.appealReason || '').trim();
  186. if (!appealReason) {
  187. throw new Error('请填写异议说明');
  188. }
  189. const requestId = String(payload.requestId || '').trim();
  190. if (!requestId) {
  191. throw new Error(
  192. '缺少客户端请求号(requestId)。请用 createIdempotentKey() 生成一次并在重试时复用'
  193. );
  194. }
  195. const grantId = toId(payload.verificationGrantId);
  196. const body = { appealReason, requestId };
  197. if (grantId != null) body.verificationGrantId = grantId;
  198. const res = await request.post(`/hr/ess/payslips/${toId(payslipId)}/appeals`, body);
  199. return unwrap(res);
  200. }